From 5c6c52cf41a23871e4dbaf0a9965be00107e8d9e Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 18 Jul 2025 18:32:55 +0900 Subject: [PATCH] =?UTF-8?q?test:=20ReservationTimeController=EC=9D=98=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=BD=94?= =?UTF-8?q?=ED=8B=80=EB=A6=B0=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/ReservationTimeControllerTest.kt | 488 ++++++++++-------- 1 file changed, 270 insertions(+), 218 deletions(-) diff --git a/src/test/java/roomescape/reservation/web/ReservationTimeControllerTest.kt b/src/test/java/roomescape/reservation/web/ReservationTimeControllerTest.kt index 4848d971..71b4040a 100644 --- a/src/test/java/roomescape/reservation/web/ReservationTimeControllerTest.kt +++ b/src/test/java/roomescape/reservation/web/ReservationTimeControllerTest.kt @@ -1,254 +1,306 @@ -package roomescape.reservation.web; +package roomescape.reservation.web -import static org.hamcrest.Matchers.*; +import com.ninjasquad.springmockk.MockkBean +import com.ninjasquad.springmockk.SpykBean +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.mockk.every +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import roomescape.common.config.JacksonConfig +import roomescape.common.exception.ErrorType +import roomescape.reservation.business.ReservationTimeService +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity +import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository +import roomescape.util.ReservationFixture +import roomescape.util.ReservationTimeFixture +import roomescape.util.RoomescapeApiTest +import roomescape.util.ThemeFixture +import java.time.LocalDate +import java.time.LocalTime -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Map; -import java.util.stream.Stream; +@WebMvcTest(ReservationTimeController::class) +@Import(JacksonConfig::class) +class ReservationTimeControllerTest( + val mockMvc: MockMvc, +) : RoomescapeApiTest() { -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.Sql.ExecutionPhase; + @SpykBean + private lateinit var reservationTimeService: ReservationTimeService -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import io.restassured.http.Header; -import roomescape.member.infrastructure.persistence.MemberEntity; -import roomescape.member.infrastructure.persistence.MemberRepository; -import roomescape.member.infrastructure.persistence.Role; -import roomescape.reservation.infrastructure.persistence.ReservationEntity; -import roomescape.reservation.infrastructure.persistence.ReservationRepository; -import roomescape.reservation.infrastructure.persistence.ReservationStatus; -import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity; -import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository; -import roomescape.theme.infrastructure.persistence.ThemeEntity; -import roomescape.theme.infrastructure.persistence.ThemeRepository; + @MockkBean + private lateinit var reservationTimeRepository: ReservationTimeRepository -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) -public class ReservationTimeControllerTest { + @MockkBean + private lateinit var reservationRepository: ReservationRepository - @Autowired - private ReservationTimeRepository reservationTimeRepository; + init { + Given("등록된 모든 시간을 조회할 때") { + val endpoint = "/times" - @Autowired - private ThemeRepository themeRepository; + When("관리자인 경우") { + beforeTest { + loginAsAdmin() + } - @Autowired - private ReservationRepository reservationRepository; + Then("정상 응답") { + every { + reservationTimeRepository.findAll() + } returns listOf( + ReservationTimeFixture.create(id = 1L), + ReservationTimeFixture.create(id = 2L) + ) - @Autowired - private MemberRepository memberRepository; + runGetTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isOk() } + content { + contentType(MediaType.APPLICATION_JSON) + jsonPath("$.data.times[0].id") { value(1) } + jsonPath("$.data.times[1].id") { value(2) } + } + } + } + } - @LocalServerPort - private int port; + When("관리자가 아닌 경우") { + loginAsUser() - private final Map params = Map.of( - "startAt", "17:00" - ); + Then("로그인 페이지로 이동") { + runGetTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + header { string("Location", "/login") } + } + } + } + } - @Test - @DisplayName("처음으로 등록하는 시간의 id는 1이다.") - void firstPost() { - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password"); + Given("시간을 추가할 때") { + val endpoint = "/times" - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header(new Header("Cookie", adminAccessTokenCookie)) - .body(params) - .when().post("/times") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/times/1"); - } + When("관리자인 경우") { + beforeTest { + loginAsAdmin() + } + val time = LocalTime.of(10, 0) + val request = ReservationTimeRequest(startAt = time) - @Test - @DisplayName("아무 시간도 등록 하지 않은 경우, 시간 목록 조회 결과 개수는 0개이다.") - void readEmptyTimes() { - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password"); + Then("시간 형식이 HH:mm이 아니거나, 범위를 벗어나면 400 응답") { + listOf( + "{\"startAt\": \"23:30:30\"}", + "{\"startAt\": \"24:59\"}", + ).forEach { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = it, + log = true + ) { + status { isBadRequest() } + } + } + } - RestAssured.given().log().all() - .port(port) - .header(new Header("Cookie", adminAccessTokenCookie)) - .when().get("/times") - .then().log().all() - .statusCode(200) - .body("data.times.size()", is(0)); - } + Then("정상 응답") { + every { + reservationTimeService.addTime(request) + } returns ReservationTimeResponse(id = 1, startAt = time) - @Test - @DisplayName("하나의 시간만 등록한 경우, 시간 목록 조회 결과 개수는 1개이다.") - void readTimesSizeAfterFirstPost() { - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password"); + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isCreated() } + content { + contentType(MediaType.APPLICATION_JSON) + jsonPath("$.data.id") { value(1) } + jsonPath("$.data.startAt") { value("10:00") } + } + } + } - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header(new Header("Cookie", adminAccessTokenCookie)) - .body(params) - .when().post("/times") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/times/1"); + Then("동일한 시간이 존재하면 409 응답") { + every { + reservationTimeRepository.existsByStartAt(time) + } returns true - RestAssured.given().log().all() - .port(port) - .header(new Header("Cookie", adminAccessTokenCookie)) - .when().get("/times") - .then().log().all() - .statusCode(200) - .body("data.times.size()", is(1)); - } + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request, + log = true + ) { + status { isConflict() } + content { + contentType(MediaType.APPLICATION_JSON) + jsonPath("$.errorType") { value(ErrorType.TIME_DUPLICATED.name) } + } + } + } + } - @Test - @DisplayName("하나의 시간만 등록한 경우, 시간 삭제 뒤 시간 목록 조회 결과 개수는 0개이다.") - void readTimesSizeAfterPostAndDelete() { - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password"); + When("관리자가 아닌 경우") { + loginAsUser() - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header(new Header("Cookie", adminAccessTokenCookie)) - .body(params) - .when().post("/times") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/times/1"); + Then("로그인 페이지로 이동") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = ReservationTimeFixture.create(), + log = true + ) { + status { is3xxRedirection() } + header { string("Location", "/login") } + } + } + } + } - RestAssured.given().log().all() - .port(port) - .header(new Header("Cookie", adminAccessTokenCookie)) - .when().delete("/times/1") - .then().log().all() - .statusCode(204); + Given("시간을 삭제할 때") { + val endpoint = "/times/1" - RestAssured.given().log().all() - .port(port) - .header(new Header("Cookie", adminAccessTokenCookie)) - .when().get("/times") - .then().log().all() - .statusCode(200) - .body("data.times.size()", is(0)); - } + When("관리자인 경우") { + beforeTest { + loginAsAdmin() + } - @ParameterizedTest - @MethodSource("validateRequestDataFormatSource") - @DisplayName("예약 시간 생성 시, 시간 요청 데이터에 시간 포맷이 아닌 값이 입력되어오면 400 에러를 발생한다.") - void validateRequestDataFormat(Map request) { - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password"); + Then("정상 응답") { + every { + reservationTimeService.removeTimeById(1L) + } returns Unit - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(request) - .when().post("/times") - .then().log().all() - .statusCode(400); - } + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { isNoContent() } + } + } - static Stream> validateRequestDataFormatSource() { - return Stream.of( - Map.of( - "startAt", "24:59" - ), - Map.of( - "startAt", "hihi") - ); - } + Then("없는 시간을 조회하면 400 응답") { + val id = 1L + every { + reservationTimeRepository.findByIdOrNull(id) + } returns null - @ParameterizedTest - @MethodSource("validateBlankRequestSource") - @DisplayName("예약 시간 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.") - void validateBlankRequest(Map request) { - String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password"); + runDeleteTest( + mockMvc = mockMvc, + endpoint = "/times/$id", + log = true + ) { + status { isBadRequest() } + content { + contentType(MediaType.APPLICATION_JSON) + jsonPath("$.errorType") { value(ErrorType.RESERVATION_TIME_NOT_FOUND.name) } + } + } + } - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .header(new Header("Cookie", adminAccessTokenCookie)) - .port(port) - .body(request) - .when().post("/times") - .then().log().all() - .statusCode(400); - } + Then("예약이 있는 시간을 삭제하면 409 응답") { + val id = 1L + every { + reservationTimeRepository.findByIdOrNull(id) + } returns ReservationTimeFixture.create(id = id) - static Stream> validateBlankRequestSource() { - return Stream.of( - Map.of( - ), - Map.of( - "startAt", "" - ), - Map.of( - "startAt", " " - ) - ); - } + every { + reservationRepository.findByReservationTime(any()) + } returns listOf(ReservationFixture.create()) - private String getAdminAccessTokenCookieByLogin(String email, String password) { - memberRepository.save(new MemberEntity(null, "이름", email, password, Role.ADMIN)); + runDeleteTest( + mockMvc = mockMvc, + endpoint = "/times/$id", + log = true + ) { + status { isConflict() } + content { + contentType(MediaType.APPLICATION_JSON) + jsonPath("$.errorType") { value(ErrorType.TIME_IS_USED_CONFLICT.name) } + } + } + } + } - Map loginParams = Map.of( - "email", email, - "password", password - ); + When("관리자가 아닌 경우") { + loginAsUser() - String accessToken = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookie("accessToken"); + Then("로그인 페이지로 이동") { + runDeleteTest( + mockMvc = mockMvc, + endpoint = endpoint, + log = true + ) { + status { is3xxRedirection() } + header { string("Location", "/login") } + } + } + } + } - return "accessToken=" + accessToken; - } + Given("날짜, 테마가 주어졌을 때") { + loginAsUser() - @Test - @DisplayName("특정 날짜의 특정 테마 예약 현황을 조회한다.") - void readReservationByDateAndThemeId() { - // given - LocalDate today = LocalDate.now(); - ReservationTimeEntity reservationTime1 = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 0))); - ReservationTimeEntity reservationTime2 = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ReservationTimeEntity reservationTime3 = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(18, 30))); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명1", "설명", "썸네일URL")); - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); + val date: LocalDate = LocalDate.now() + val themeId = 1L - reservationRepository.save( - new ReservationEntity(null, today.plusDays(1), reservationTime1, theme, member, - ReservationStatus.CONFIRMED)); - reservationRepository.save( - new ReservationEntity(null, today.plusDays(1), reservationTime2, theme, member, - ReservationStatus.CONFIRMED)); - reservationRepository.save( - new ReservationEntity(null, today.plusDays(1), reservationTime3, theme, member, - ReservationStatus.CONFIRMED)); + When("저장된 예약 시간이 있으면") { + val times: List = listOf( + ReservationTimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)), + ReservationTimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0)) + ) - // when & then - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header("Cookie", getAdminAccessTokenCookieByLogin("a@a.a", "a")) - .when().get("/times/filter?date={date}&themeId={themeId}", today.plusDays(1).toString(), theme.getId()) - .then().log().all() - .statusCode(200) - .body("data.reservationTimes.size()", is(3)); - } + every { + reservationTimeRepository.findAll() + } returns times + + Then("그 시간과, 해당 날짜와 테마에 대한 예약 여부가 담긴 목록을 응답") { + + every { + reservationRepository.findByDateAndThemeId(date, themeId) + } returns listOf( + ReservationFixture.create( + id = 1L, + date = date, + theme = ThemeFixture.create(id = themeId), + reservationTime = times[0] + ) + ) + + val response = runGetTest( + mockMvc = mockMvc, + endpoint = "/times/filter?date=$date&themeId=$themeId", + log = true + ) { + status { isOk() } + content { + contentType(MediaType.APPLICATION_JSON) + } + }.andReturn().readValue(ReservationTimeInfosResponse::class.java) + + assertSoftly(response.times) { + this shouldHaveSize times.size + this[0].id shouldBe times[0].id + this[0].alreadyBooked shouldBe true + + this[1].id shouldBe times[1].id + this[1].alreadyBooked shouldBe false + } + } + } + } + } }