From 4b3afd12fffb40f827a9380e7fbe250b4e34868b Mon Sep 17 00:00:00 2001 From: pricelees Date: Mon, 21 Jul 2025 20:58:18 +0900 Subject: [PATCH] =?UTF-8?q?test:=20ReservationControllerTest=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/ReservationControllerTest.kt | 1438 +++++++++-------- 1 file changed, 788 insertions(+), 650 deletions(-) diff --git a/src/test/java/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/java/roomescape/reservation/web/ReservationControllerTest.kt index fb382f10..f4fbfffe 100644 --- a/src/test/java/roomescape/reservation/web/ReservationControllerTest.kt +++ b/src/test/java/roomescape/reservation/web/ReservationControllerTest.kt @@ -1,654 +1,792 @@ -package roomescape.reservation.web; +package roomescape.reservation.web -import static org.assertj.core.api.Assertions.*; -import static org.hamcrest.Matchers.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.util.Map; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.MockitoAnnotations; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.Sql.ExecutionPhase; - -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.payment.infrastructure.client.TossPaymentClient; -import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity; -import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository; -import roomescape.payment.infrastructure.persistence.PaymentEntity; -import roomescape.payment.infrastructure.persistence.PaymentRepository; -import roomescape.payment.web.PaymentApprove; -import roomescape.payment.web.PaymentCancel; -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; +import com.ninjasquad.springmockk.MockkBean +import com.ninjasquad.springmockk.SpykBean +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import jakarta.persistence.EntityManager +import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.equalTo +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.transaction.support.TransactionTemplate +import roomescape.auth.web.support.AdminInterceptor +import roomescape.auth.web.support.LoginInterceptor +import roomescape.auth.web.support.MemberIdResolver +import roomescape.common.exception.ErrorType +import roomescape.common.exception.RoomescapeException +import roomescape.member.infrastructure.persistence.MemberEntity +import roomescape.member.infrastructure.persistence.Role +import roomescape.payment.infrastructure.client.TossPaymentClient +import roomescape.payment.infrastructure.persistence.PaymentEntity +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.util.* +import java.time.LocalDate +import java.time.LocalTime @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) -public class ReservationControllerTest { - - @Autowired - private ReservationRepository reservationRepository; - @Autowired - private ReservationTimeRepository reservationTimeRepository; - @Autowired - private ThemeRepository themeRepository; - @Autowired - private MemberRepository memberRepository; - @Autowired - private PaymentRepository paymentRepository; - @Autowired - private CanceledPaymentRepository canceledPaymentRepository; - - @MockBean - private TossPaymentClient paymentClient; - - @LocalServerPort - private int port; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - @DisplayName("처음으로 등록하는 예약의 id는 1이다.") - void firstPost() { - String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234"); - - LocalTime time = LocalTime.of(17, 30); - LocalDate date = LocalDate.now().plusDays(1L); - - reservationTimeRepository.save(new ReservationTimeEntity(null, time)); - themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - - Map reservationParams = Map.of( - "date", date.toString(), - "timeId", "1", - "themeId", "1", - "paymentKey", "pk", - "orderId", "oi", - "amount", "1000", - "paymentType", "DEFAULT" - ); - - when(paymentClient.confirmPayment(any(PaymentApprove.Request.class))).thenReturn( - new PaymentApprove.Response("pk", "oi", OffsetDateTime.of(date, time, ZoneOffset.ofHours(9)), 1000L)); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header("Cookie", accessTokenCookie) - .body(reservationParams) - .when().post("/reservations") - .then().log().all() - .statusCode(201) - .body("data.id", is(1)) - .header("Location", "/reservations/1"); - } - - @Test - @DisplayName("대기중인 예약을 취소한다.") - void cancelWaiting() { - // given - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); - - ReservationTimeEntity reservationTime = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity member1 = memberRepository.save( - new MemberEntity(null, "name1", "email1r@email.com", "password", Role.MEMBER)); - - // when - reservationRepository.save( - new ReservationEntity(null, LocalDate.now().plusDays(1), reservationTime, theme, member1, - ReservationStatus.CONFIRMED)); - ReservationEntity waiting = reservationRepository.save( - new ReservationEntity(null, LocalDate.now().plusDays(1), reservationTime, theme, member, - ReservationStatus.WAITING)); - - // then - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessTokenCookie) - .when().delete("/reservations/waiting/{id}", waiting.getId()) - .then().log().all() - .statusCode(204); - } - - @Test - @DisplayName("회원은 자신이 아닌 다른 회원의 예약을 취소할 수 없다.") - void cantCancelOtherMembersWaiting() { - // given - MemberEntity confirmedMember = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); - - ReservationTimeEntity reservationTime = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity waitingMember = memberRepository.save( - new MemberEntity(null, "name1", "email1r@email.com", "password", Role.MEMBER)); - - // when - reservationRepository.save( - new ReservationEntity(null, LocalDate.now().plusDays(1), reservationTime, theme, confirmedMember, - ReservationStatus.CONFIRMED)); - ReservationEntity waiting = reservationRepository.save( - new ReservationEntity(null, LocalDate.now().plusDays(1), reservationTime, theme, waitingMember, - ReservationStatus.WAITING)); - - // then - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessTokenCookie) - .when().delete("/reservations/waiting/{id}", waiting.getId()) - .then().log().all() - .statusCode(404); - } - - @Test - @DisplayName("관리자 권한이 있으면 전체 예약정보를 조회할 수 있다.") - void readEmptyReservations() { - // given - String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234"); - - ReservationTimeEntity reservationTime = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - - // when - reservationRepository.save( - new ReservationEntity(null, LocalDate.now(), reservationTime, theme, member, ReservationStatus.CONFIRMED)); - reservationRepository.save( - new ReservationEntity(null, LocalDate.now().plusDays(1), reservationTime, theme, member, - ReservationStatus.CONFIRMED)); - reservationRepository.save( - new ReservationEntity(null, LocalDate.now().plusDays(2), reservationTime, theme, member, - ReservationStatus.CONFIRMED)); - - // then - RestAssured.given().log().all() - .port(port) - .header(new Header("Cookie", accessTokenCookie)) - .when().get("/reservations") - .then().log().all() - .statusCode(200) - .body("data.reservations.size()", is(3)); - } - - @Test - @DisplayName("예약 취소는 관리자만 할 수 있다.") - void canRemoveMyReservation() { - // given - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); - - ReservationTimeEntity reservationTime = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - ReservationEntity reservation = reservationRepository.save( - new ReservationEntity(null, LocalDate.now(), reservationTime, theme, member, ReservationStatus.CONFIRMED)); - - // when & then - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessTokenCookie) - .when().delete("/reservations/" + reservation.getId()) - .then().log().all() - .statusCode(302); - } - - @Test - @DisplayName("관리자가 대기중인 예약을 거절한다.") - void denyWaiting() { - // given - String adminTokenCookie = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); - - ReservationTimeEntity reservationTime = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity confirmedMember = memberRepository.save( - new MemberEntity(null, "name1", "email@email.com", "password", Role.MEMBER)); - MemberEntity waitingMember = memberRepository.save( - new MemberEntity(null, "name1", "email1@email.com", "password", Role.MEMBER)); - - reservationRepository.save( - new ReservationEntity(null, LocalDate.now(), reservationTime, theme, confirmedMember, - ReservationStatus.CONFIRMED)); - ReservationEntity waiting = reservationRepository.save( - new ReservationEntity(null, LocalDate.now(), reservationTime, theme, waitingMember, - ReservationStatus.WAITING)); - - // when & then - RestAssured.given().log().all() - .port(port) - .header("Cookie", adminTokenCookie) - .when().post("/reservations/waiting/{id}/deny", waiting.getId()) - .then().log().all() - .statusCode(204); - } - - @Test - @DisplayName("본인의 예약이 아니더라도 관리자 권한이 있으면 예약 정보를 삭제할 수 있다.") - void readReservationsSizeAfterPostAndDelete() { - // given - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "admin@admin.com", "password", Role.ADMIN)); - String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); - - ReservationTimeEntity reservationTime = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity anotherMember = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - - ReservationEntity reservation = reservationRepository.save( - new ReservationEntity(null, LocalDate.now(), reservationTime, theme, anotherMember, - ReservationStatus.CONFIRMED)); - - // when & then - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessTokenCookie) - .when().delete("/reservations/" + reservation.getId()) - .then().log().all() - .statusCode(204); - } - - @ParameterizedTest - @MethodSource("requestValidateSource") - @DisplayName("예약 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.") - void validateBlankRequest(Map invalidRequestBody) { - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(invalidRequestBody) - .header("Cookie", getAdminAccessTokenCookieByLogin("a@a.a", "a")) - .when().post("/reservations") - .then().log().all() - .statusCode(400); - } - - private static Stream> requestValidateSource() { - return Stream.of( - Map.of("timeId", "1", - "themeId", "1"), - - Map.of("date", LocalDate.now().plusDays(1L).toString(), - "themeId", "1"), - - Map.of("date", LocalDate.now().plusDays(1L).toString(), - "timeId", "1"), - - Map.of("date", " ", - "timeId", "1", - "themeId", "1"), - - Map.of("date", LocalDate.now().plusDays(1L).toString(), - "timeId", " ", - "themeId", "1"), - - Map.of("date", LocalDate.now().plusDays(1L).toString(), - "timeId", "1", - "themeId", " ") - ); - } - - @Test - @DisplayName("예약 생성 시, 정수 요청 데이터에 문자가 입력되어오면 400 에러를 발생한다.") - void validateRequestDataFormat() { - Map invalidTypeRequestBody = Map.of( - "date", LocalDate.now().plusDays(1L).toString(), - "timeId", "1", - "themeId", "한글" - ); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header("Cookie", getAdminAccessTokenCookieByLogin("a@a.a", "a")) - .body(invalidTypeRequestBody) - .when().post("/reservations") - .then().log().all() - .statusCode(400); - } - - @ParameterizedTest - @DisplayName("모든 예약 / 대기 중인 예약 / 현재 로그인된 회원의 예약 및 대기를 조회한다.") - @CsvSource(value = {"/reservations, reservations, 2", "/reservations/waiting, reservations, 1", - "/reservations-mine, myReservationResponses, 3"}, delimiter = ',') - void getAllReservations(String requestURI, String responseFieldName, int expectedSize) { - // given - LocalDate date = LocalDate.now().plusDays(1); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - ReservationTimeEntity time = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - ReservationTimeEntity time1 = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(18, 30))); - ReservationTimeEntity time2 = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(19, 30))); - - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.ADMIN)); - String accessToken = getAccessTokenCookieByLogin("email@email.com", "password"); - - // when : 예약은 2개, 예약 대기는 1개 조회되어야 한다. - reservationRepository.save(new ReservationEntity(null, date, time, theme, member, ReservationStatus.CONFIRMED)); - reservationRepository.save( - new ReservationEntity(null, date, time1, theme, member, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)); - reservationRepository.save(new ReservationEntity(null, date, time2, theme, member, ReservationStatus.WAITING)); - - // then - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessToken) - .when().get(requestURI) - .then().log().all() - .statusCode(200) - .body("data." + responseFieldName + ".size()", is(expectedSize)); - } - - @Test - @DisplayName("예약을 삭제할 때, 승인되었으나 결제 대기중인 예약은 결제 취소 없이 바로 삭제한다.") - void removeNotPaidReservation() { - // given - LocalDate date = LocalDate.now().plusDays(1); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - ReservationTimeEntity time = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); - - // when - ReservationEntity saved = reservationRepository.save(new ReservationEntity(null, date, time, theme, - memberRepository.save(new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)), - ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)); - - // then - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessToken) - .when().delete("/reservations/{id}", saved.getId()) - .then().log().all() - .statusCode(204); - } - - @Test - @DisplayName("이미 결제가 된 예약은 삭제 후 결제 취소를 요청한다.") - void removePaidReservation() { - // given - String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); - LocalDate date = LocalDate.now().plusDays(1); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - ReservationTimeEntity time = reservationTimeRepository.save( - new ReservationTimeEntity(null, LocalTime.of(17, 30))); - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - - ReservationEntity saved = reservationRepository.save( - new ReservationEntity(null, date, time, theme, member, ReservationStatus.CONFIRMED)); - PaymentEntity savedPaymentEntity = paymentRepository.save( - new PaymentEntity(null, "pk", "oi", 1000L, saved, OffsetDateTime.now().minusHours(1L))); - - // when - when(paymentClient.cancelPayment(any(PaymentCancel.Request.class))) - .thenReturn(new PaymentCancel.Response("pk", "고객 요청", savedPaymentEntity.getTotalAmount(), - OffsetDateTime.now())); - - // then - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessToken) - .when().delete("/reservations/{id}", saved.getId()) - .then().log().all() - .statusCode(204); - - } - - @Test - @DisplayName("예약을 추가할 때, 결제 승인 이후에 예외가 발생하면 결제를 취소한 뒤 결제 취소 테이블에 취소 정보를 저장한다.") - void saveReservationWithCancelPayment() { - // given - LocalDateTime localDateTime = LocalDateTime.now().minusHours(1L).withNano(0); - LocalDate date = localDateTime.toLocalDate(); - ReservationTimeEntity time = reservationTimeRepository.save( - new ReservationTimeEntity(null, localDateTime.toLocalTime())); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); - - // when : 이전 날짜의 예약을 추가하여 결제 승인 이후 DB 저장 과정에서 예외를 발생시킨다. - String paymentKey = "pk"; - OffsetDateTime canceledAt = OffsetDateTime.now().plusHours(1L).withNano(0); - OffsetDateTime approvedAt = OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(9)); - when(paymentClient.confirmPayment(any(PaymentApprove.Request.class))) - .thenReturn(new PaymentApprove.Response(paymentKey, "oi", approvedAt, 1000L)); - - when(paymentClient.cancelPayment(any(PaymentCancel.Request.class))) - .thenReturn(new PaymentCancel.Response(paymentKey, "고객 요청", 1000L, canceledAt)); - - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header("Cookie", accessToken) - .body(new ReservationRequest(date, time.getId(), theme.getId(), "pk", "oi", 1000L, "DEFAULT")) - .when().post("/reservations") - .then().log().all() - .statusCode(400); - - // then - CanceledPaymentEntity canceledPayment = canceledPaymentRepository.findByPaymentKey(paymentKey); - assertThat(canceledPayment).isNotNull(); - assertThat(canceledPayment.getCanceledAt()).isEqualTo(canceledAt); - assertThat(canceledPayment.getCancelReason()).isEqualTo("고객 요청"); - assertThat(canceledPayment.getCancelAmount()).isEqualTo(1000L); - assertThat(canceledPayment.getApprovedAt()).isEqualTo(approvedAt); - } - - @DisplayName("테마만을 이용하여 예약을 조회한다.") - @ParameterizedTest(name = "테마 ID={0}로 조회 시 {1}개의 예약이 조회된다.") - @CsvSource(value = {"1/4", "2/3"}, delimiter = '/') - @Sql({"/truncate.sql", "/test_search_data.sql"}) - void searchByTheme(String themeId, int expectedCount) { - RestAssured.given().log().all() - .port(port) - .param("themeId", themeId) - .param("memberId", "") - .param("dateFrom", "") - .param("dateTo", "") - .header("cookie", getAdminAccessTokenCookieByLogin("admin@email.com", "password")) - .when().get("/reservations/search") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .body("data.reservations.size()", is(expectedCount)); - } - - @DisplayName("시작 날짜만을 이용하여 예약을 조회한다.") - @ParameterizedTest(name = "오늘 날짜보다 {0}일 전인 날짜를 시작 날짜로 조회 시 {1}개의 예약이 조회된다.") - @CsvSource(value = {"1/1", "7/7"}, delimiter = '/') - @Sql({"/truncate.sql", "/test_search_data.sql"}) - void searchByFromDate(int minusDays, int expectedCount) { - RestAssured.given().log().all() - .port(port) - .param("themeId", "") - .param("memberId", "") - .param("dateFrom", LocalDate.now().minusDays(minusDays).toString()) - .param("dateTo", "") - .header("cookie", getAdminAccessTokenCookieByLogin("admin@email.com", "password")) - .when().get("/reservations/search") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .body("data.reservations.size()", is(expectedCount)); - } - - @DisplayName("종료 날짜만을 이용하여 예약을 조회한다..") - @ParameterizedTest(name = "오늘 날짜보다 {0}일 전인 날짜를 종료 날짜로 조회 시 {1}개의 예약이 조회된다.") - @CsvSource(value = {"1/7", "3/5", "7/1"}, delimiter = '/') - @Sql({"/truncate.sql", "/test_search_data.sql"}) - void searchByToDate(int minusDays, int expectedCount) { - RestAssured.given().log().all() - .port(port) - .param("themeId", "") - .param("memberId", "") - .param("dateFrom", "") - .param("dateTo", LocalDate.now().minusDays(minusDays).toString()) - .header("cookie", getAdminAccessTokenCookieByLogin("admin@email.com", "password")) - .when().get("/reservations/search") - .then().log().all() - .statusCode(HttpStatus.OK.value()) - .body("data.reservations.size()", is(expectedCount)); - } - - @Test - @DisplayName("예약 대기를 추가한다.") - void addWaiting() { - // given - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); - LocalDate date = localDateTime.toLocalDate(); - ReservationTimeEntity time = reservationTimeRepository.save( - new ReservationTimeEntity(null, localDateTime.toLocalTime())); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - MemberEntity member1 = memberRepository.save( - new MemberEntity(null, "name1", "email1@email.com", "password", Role.MEMBER)); - - String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); - reservationRepository.save( - new ReservationEntity(null, date, time, theme, member1, ReservationStatus.CONFIRMED)); - - // when & then - RestAssured.given().log().all() - .port(port) - .contentType(ContentType.JSON) - .header("Cookie", accessToken) - .body(new WaitingRequest(date, time.getId(), theme.getId())) - .when().post("/reservations/waiting") - .then().log().all() - .statusCode(201) - .body("data.status", is("WAITING")); - } - - @Test - @DisplayName("대기중인 예약을 승인한다.") - void approveWaiting() { - // given - LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L); - LocalDate date = localDateTime.toLocalDate(); - ReservationTimeEntity time = reservationTimeRepository.save( - new ReservationTimeEntity(null, localDateTime.toLocalTime())); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - - String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); - ReservationEntity waiting = reservationRepository.save( - new ReservationEntity(null, date, time, theme, member, ReservationStatus.WAITING)); - - // when - RestAssured.given().log().all() - .port(port) - .header("Cookie", accessToken) - .when().post("/reservations/waiting/{id}/approve", waiting.getId()) - .then().log().all() - .statusCode(200); - - // then - reservationRepository.findById(waiting.getId()) - .ifPresent(r -> assertThat(r.getReservationStatus()).isEqualTo( - ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)); - - } - - private String getAccessTokenCookieByLogin(final String email, final String password) { - Map loginParams = Map.of( - "email", email, - "password", password - ); - - String accessToken = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookie("accessToken"); - - return "accessToken=" + accessToken; - } - - @Test - @DisplayName("관리자가 직접 예약을 추가한다.") - void addReservationByAdmin() { - // given - LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); - LocalDate date = localDateTime.toLocalDate(); - ReservationTimeEntity time = reservationTimeRepository.save( - new ReservationTimeEntity(null, localDateTime.toLocalTime())); - ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL")); - MemberEntity member = memberRepository.save( - new MemberEntity(null, "name", "email@email.com", "password", Role.MEMBER)); - - String adminAccessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); - - // when & then - RestAssured.given().log().all() - .port(port) - .contentType(ContentType.JSON) - .header("Cookie", adminAccessToken) - .body(new AdminReservationRequest(date, time.getId(), theme.getId(), member.getId())) - .when().post("/reservations/admin") - .then().log().all() - .statusCode(201); - } - - private String getAdminAccessTokenCookieByLogin(final String email, final String password) { - memberRepository.save(new MemberEntity(null, "이름", email, password, Role.ADMIN)); - - Map loginParams = Map.of( - "email", email, - "password", password - ); - - String accessToken = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookie("accessToken"); - - return "accessToken=" + accessToken; - } +class ReservationControllerTest( + @LocalServerPort val port: Int, + val entityManager: EntityManager, + val transactionTemplate: TransactionTemplate +) : FunSpec({ + extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_EACH_TEST)) +}) { + @MockkBean + lateinit var paymentClient: TossPaymentClient + + @SpykBean + lateinit var loginInterceptor: LoginInterceptor + + @SpykBean + lateinit var adminInterceptor: AdminInterceptor + + @SpykBean + lateinit var memberIdResolver: MemberIdResolver + + init { + context("POST /reservations") { + lateinit var member: MemberEntity + beforeTest { + member = login(MemberFixture.create(role = Role.MEMBER)) + } + + test("정상 응답") { + val reservationRequest = createRequest() + val paymentApproveResponse = PaymentFixture.createApproveResponse().copy( + paymentKey = reservationRequest.paymentKey, + orderId = reservationRequest.orderId, + totalAmount = reservationRequest.amount, + ) + + every { + paymentClient.confirmPayment(any()) + } returns paymentApproveResponse + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + body(reservationRequest) + }.When { + post("/reservations") + }.Then { + log().all() + statusCode(201) + body("data.date", equalTo(reservationRequest.date.toString())) + body("data.status", equalTo(ReservationStatus.CONFIRMED.name)) + } + } + + test("결제 과정에서 발생하는 에러는 그대로 응답") { + val reservationRequest = createRequest() + val paymentException = RoomescapeException( + ErrorType.PAYMENT_SERVER_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR + ) + + every { + paymentClient.confirmPayment(any()) + } throws paymentException + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + body(reservationRequest) + }.When { + post("/reservations") + }.Then { + log().all() + statusCode(paymentException.httpStatus.value()) + body("errorType", equalTo(paymentException.errorType.name)) + } + } + + test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답") { + val reservationRequest = createRequest() + val paymentApproveResponse = PaymentFixture.createApproveResponse().copy( + paymentKey = reservationRequest.paymentKey, + orderId = reservationRequest.orderId, + totalAmount = reservationRequest.amount, + ) + + every { + paymentClient.confirmPayment(any()) + } returns paymentApproveResponse + + // 예약 저장 과정에서 테마가 없는 예외 + val invalidRequest = reservationRequest.copy(themeId = reservationRequest.themeId + 1) + val expectedException = RoomescapeException(ErrorType.THEME_NOT_FOUND, HttpStatus.BAD_REQUEST) + + every { + paymentClient.cancelPayment(any()) + } returns PaymentFixture.createCancelResponse() + + val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery( + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java + ).singleResult + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + body(invalidRequest) + }.When { + post("/reservations") + }.Then { + log().all() + statusCode(expectedException.httpStatus.value()) + body("errorType", equalTo(expectedException.errorType.name)) + } + + val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery( + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java + ).singleResult + + canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L + } + } + + context("GET /reservations") { + lateinit var reservations: MutableMap> + beforeTest { + reservations = createDummyReservations() + } + + test("관리자이면 정상 응답") { + login(MemberFixture.create(role = Role.ADMIN)) + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + }.When { + get("/reservations") + }.Then { + log().all() + statusCode(200) + body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size })) + } + } + } + + context("GET /reservations-mine") { + lateinit var reservations: MutableMap> + beforeTest { + reservations = createDummyReservations() + } + + test("로그인한 회원이 자신의 예약 목록을 조회한다.") { + val member: MemberEntity = login(reservations.keys.first()) + val expectedReservations: Int = reservations[member]?.size ?: 0 + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + }.When { + get("/reservations-mine") + }.Then { + log().all() + statusCode(200) + body("data.reservations.size()", equalTo(expectedReservations)) + } + } + } + + context("GET /reservations/search") { + lateinit var reservations: MutableMap> + beforeTest { + reservations = createDummyReservations() + } + + test("관리자만 검색할 수 있다.") { + login(reservations.keys.first()) + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + }.When { + get("/reservations/search") + }.Then { + log().all() + header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE)) + } + } + + test("파라미터를 지정하지 않으면 전체 목록 응답") { + login(MemberFixture.create(role = Role.ADMIN)) + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + }.When { + get("/reservations/search") + }.Then { + log().all() + statusCode(200) + body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size })) + } + } + + test("시작 날짜가 종료 날짜 이전이면 예외 응답") { + login(MemberFixture.create(role = Role.ADMIN)) + + val startDate = LocalDate.now().plusDays(1) + val endDate = LocalDate.now() + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + param("dateFrom", startDate.toString()) + param("dateTo", endDate.toString()) + }.When { + get("/reservations/search") + }.Then { + log().all() + statusCode(HttpStatus.BAD_REQUEST.value()) + body("errorType", equalTo(ErrorType.INVALID_DATE_RANGE.name)) + } + } + + test("동일한 회원의 모든 예약 응답") { + login(MemberFixture.create(role = Role.ADMIN)) + val member: MemberEntity = reservations.keys.first() + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + param("memberId", member.id) + }.When { + get("/reservations/search") + }.Then { + log().all() + statusCode(200) + body("data.reservations.size()", equalTo(reservations[member]?.size ?: 0)) + } + } + + test("동일한 테마의 모든 예약 응답") { + login(MemberFixture.create(role = Role.ADMIN)) + val themes = reservations.values.flatten().map { it.theme } + val requestThemeId: Long = themes.first().id!! + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + param("themeId", requestThemeId) + }.When { + get("/reservations/search") + }.Then { + log().all() + statusCode(200) + body("data.reservations.size()", equalTo(themes.filter { it.id == requestThemeId }.size)) + } + } + + test("시작 날짜와 종료 날짜 사이의 예약 응답") { + login(MemberFixture.create(role = Role.ADMIN)) + val dateFrom: LocalDate = reservations.values.flatten().minOf { it.date } + val dateTo: LocalDate = reservations.values.flatten().maxOf { it.date } + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + param("dateFrom", dateFrom.toString()) + param("dateTo", dateTo.toString()) + }.When { + get("/reservations/search") + }.Then { + log().all() + statusCode(200) + body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size })) + } + } + } + + context("DELETE /reservations/{id}") { + lateinit var reservations: MutableMap> + beforeTest { + reservations = createDummyReservations() + } + + test("관리자만 예약을 삭제할 수 있다.") { + login(MemberFixture.create(role = Role.MEMBER)) + val reservation: ReservationEntity = reservations.values.flatten().first() + + Given { + port(port) + }.When { + delete("/reservations/${reservation.id}") + }.Then { + log().all() + statusCode(302) + header(HttpHeaders.LOCATION, containsString("/login")) + } + } + + test("결제되지 않은 예약은 바로 제거") { + login(MemberFixture.create(role = Role.ADMIN)) + val reservationId: Long = reservations.values.flatten().first().id!! + + transactionTemplate.execute { + val reservation: ReservationEntity = entityManager.find( + ReservationEntity::class.java, + reservationId + ) + reservation.reservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + entityManager.persist(reservation) + entityManager.flush() + entityManager.clear() + } + + Given { + port(port) + }.When { + delete("/reservations/$reservationId") + }.Then { + log().all() + statusCode(HttpStatus.NO_CONTENT.value()) + } + + // 예약이 삭제되었는지 확인 + transactionTemplate.executeWithoutResult { + val deletedReservation = entityManager.find( + ReservationEntity::class.java, + reservationId + ) + deletedReservation shouldBe null + } + } + + test("결제된 예약은 취소 후 제거") { + login(MemberFixture.create(role = Role.ADMIN)) + val reservation: ReservationEntity = reservations.values.flatten().first() + lateinit var payment: PaymentEntity + + transactionTemplate.execute { + payment = PaymentFixture.create(reservation = reservation).also { + entityManager.persist(it) + entityManager.flush() + entityManager.clear() + } + } + + every { + paymentClient.cancelPayment(any()) + } returns PaymentFixture.createCancelResponse() + + val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery( + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java + ).singleResult + + Given { + port(port) + }.When { + delete("/reservations/${reservation.id}") + }.Then { + log().all() + statusCode(HttpStatus.NO_CONTENT.value()) + } + + val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery( + "SELECT COUNT(c) FROM CanceledPaymentEntity c", + Long::class.java + ).singleResult + + canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L + } + } + + context("POST /reservations/admin") { + test("관리자가 예약을 추가하면 결제 대기 상태로 예약 생성") { + val member = login(MemberFixture.create(role = Role.ADMIN)) + val adminRequest: AdminReservationRequest = createRequest().let { + AdminReservationRequest( + date = it.date, + themeId = it.themeId, + timeId = it.timeId, + memberId = member.id!!, + ) + } + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + body(adminRequest) + }.When { + post("/reservations/admin") + }.Then { + log().all() + statusCode(201) + body("data.status", equalTo(ReservationStatus.CONFIRMED_PAYMENT_REQUIRED.name)) + } + } + } + + context("GET /reservations/waiting") { + lateinit var reservations: MutableMap> + beforeTest { + reservations = createDummyReservations() + } + + test("관리자가 아니면 조회할 수 없다.") { + login(MemberFixture.create(role = Role.MEMBER)) + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + }.When { + get("/reservations/waiting") + }.Then { + log().all() + header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE)) + } + } + + test("대기 중인 예약 목록을 조회한다.") { + login(MemberFixture.create(role = Role.ADMIN)) + val expected = reservations.values.flatten() + .count { it.reservationStatus == ReservationStatus.WAITING } + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + }.When { + get("/reservations/waiting") + }.Then { + log().all() + statusCode(200) + body("data.reservations.size()", equalTo(expected)) + } + } + } + + context("POST /reservations/waiting") { + test("회원이 대기 예약을 추가한다.") { + val member = login(MemberFixture.create(role = Role.MEMBER)) + val waitingRequest: WaitingRequest = createRequest().let { + WaitingRequest( + date = it.date, + themeId = it.themeId, + timeId = it.timeId + ) + } + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + body(waitingRequest) + }.When { + post("/reservations/waiting") + }.Then { + log().all() + statusCode(201) + body("data.member.id", equalTo(member.id!!.toInt())) + body("data.status", equalTo(ReservationStatus.WAITING.name)) + } + } + + test("이미 예약된 시간, 테마로 대기 예약 요청 시 예외 응답") { + val member = login(MemberFixture.create(role = Role.MEMBER)) + val reservationRequest = createRequest() + + transactionTemplate.executeWithoutResult { + val reservation = ReservationFixture.create( + date = reservationRequest.date, + theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId), + reservationTime = entityManager.find(ReservationTimeEntity::class.java, reservationRequest.timeId), + member = member, + status = ReservationStatus.WAITING + ) + entityManager.persist(reservation) + entityManager.flush() + entityManager.clear() + } + + // 이미 예약된 시간, 테마로 대기 예약 요청 + val waitingRequest = WaitingRequest( + date = reservationRequest.date, + themeId = reservationRequest.themeId, + timeId = reservationRequest.timeId + ) + + Given { + port(port) + contentType(MediaType.APPLICATION_JSON_VALUE) + body(waitingRequest) + }.When { + post("/reservations/waiting") + }.Then { + log().all() + statusCode(HttpStatus.BAD_REQUEST.value()) + body("errorType", equalTo(ErrorType.HAS_RESERVATION_OR_WAITING.name)) + } + } + } + + context("DELETE /reservations/waiting/{id}") { + lateinit var reservations: MutableMap> + beforeTest { + reservations = createDummyReservations() + } + + test("대기 중인 예약을 취소한다.") { + val member = login(MemberFixture.create(role = Role.MEMBER)) + val waiting: ReservationEntity = createSingleReservation( + member = member, + status = ReservationStatus.WAITING + ) + + Given { + port(port) + }.When { + delete("/reservations/waiting/${waiting.id}") + }.Then { + log().all() + statusCode(HttpStatus.NO_CONTENT.value()) + } + + transactionTemplate.executeWithoutResult { _ -> + entityManager.find( + ReservationEntity::class.java, + waiting.id + ) shouldBe null + } + } + + test("이미 완료된 예약은 삭제할 수 없다.") { + val member = login(MemberFixture.create(role = Role.MEMBER)) + val reservation: ReservationEntity = createSingleReservation( + member = member, + status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + ) + + Given { + port(port) + }.When { + delete("/reservations/waiting/{id}", reservation.id) + }.Then { + log().all() + body("errorType", equalTo(ErrorType.RESERVATION_NOT_FOUND.name)) + statusCode(HttpStatus.NOT_FOUND.value()) + } + } + } + + context("POST /reservations/waiting/{id}/approve") { + test("관리자만 승인할 수 있다.") { + login(MemberFixture.create(role = Role.MEMBER)) + + Given { + port(port) + }.When { + post("/reservations/waiting/1/approve") + }.Then { + log().all() + statusCode(302) + header(HttpHeaders.LOCATION, containsString("/login")) + } + } + + test("대기 예약을 승인하면 결제 대기 상태로 변경") { + val member = login(MemberFixture.create(role = Role.ADMIN)) + val reservation = createSingleReservation( + member = member, + status = ReservationStatus.WAITING + ) + + Given { + port(port) + }.When { + post("/reservations/waiting/${reservation.id!!}/approve") + }.Then { + log().all() + statusCode(200) + } + + transactionTemplate.executeWithoutResult { _ -> + entityManager.find( + ReservationEntity::class.java, + reservation.id + )?.also { + it.reservationStatus shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + } ?: throw AssertionError("Reservation not found") + } + } + } + + context("POST /reservations/waiting/{id}/deny") { + test("관리자만 거절할 수 있다.") { + login(MemberFixture.create(role = Role.MEMBER)) + + Given { + port(port) + }.When { + post("/reservations/waiting/1/deny") + }.Then { + log().all() + statusCode(302) + header(HttpHeaders.LOCATION, containsString("/login")) + } + } + + test("거절된 예약은 삭제된다.") { + val member = login(MemberFixture.create(role = Role.ADMIN)) + val reservation = createSingleReservation( + member = member, + status = ReservationStatus.WAITING + ) + + Given { + port(port) + }.When { + post("/reservations/waiting/${reservation.id!!}/deny") + }.Then { + log().all() + statusCode(204) + } + + transactionTemplate.executeWithoutResult { _ -> + entityManager.find( + ReservationEntity::class.java, + reservation.id + ) shouldBe null + } + } + } + } + + fun createSingleReservation( + date: LocalDate = LocalDate.now().plusDays(1), + time: LocalTime = LocalTime.now(), + themeName: String = "Default Theme", + member: MemberEntity = MemberFixture.create(role = Role.MEMBER), + status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + ): ReservationEntity { + return ReservationFixture.create( + date = date, + theme = ThemeFixture.create(name = themeName), + reservationTime = ReservationTimeFixture.create(startAt = time), + member = member, + status = status + ).also { it -> + transactionTemplate.execute { _ -> + if (member.id == null) { + entityManager.persist(member) + } + entityManager.persist(it.reservationTime) + entityManager.persist(it.theme) + entityManager.persist(it) + entityManager.flush() + entityManager.clear() + } + } + } + + fun createDummyReservations(): MutableMap> { + val reservations: MutableMap> = mutableMapOf() + val members: List = listOf( + MemberFixture.create(role = Role.MEMBER), + MemberFixture.create(role = Role.MEMBER) + ) + + transactionTemplate.executeWithoutResult { + members.forEach { member -> + entityManager.persist(member) + } + entityManager.flush() + entityManager.clear() + } + + transactionTemplate.executeWithoutResult { + repeat(10) { index -> + val theme = ThemeFixture.create(name = "theme$index") + val time = ReservationTimeFixture.create(startAt = LocalTime.now().plusMinutes(index.toLong())) + entityManager.persist(theme) + entityManager.persist(time) + + val reservation = ReservationFixture.create( + date = LocalDate.now().plusDays(index.toLong()), + theme = theme, + reservationTime = time, + member = members[index % members.size], + status = ReservationStatus.CONFIRMED + ) + entityManager.persist(reservation) + reservations.getOrPut(reservation.member) { mutableListOf() }.add(reservation) + } + entityManager.flush() + entityManager.clear() + } + + return reservations + } + + fun createRequest( + theme: ThemeEntity = ThemeFixture.create(), + time: ReservationTimeEntity = ReservationTimeFixture.create(), + ): ReservationRequest { + lateinit var reservationRequest: ReservationRequest + + transactionTemplate.executeWithoutResult { + entityManager.persist(theme) + entityManager.persist(time) + + reservationRequest = ReservationFixture.createRequest( + themeId = theme.id!!, + timeId = time.id!!, + ) + + entityManager.flush() + entityManager.clear() + } + + return reservationRequest + } + + fun login(member: MemberEntity): MemberEntity { + if (member.id == null) { + transactionTemplate.executeWithoutResult { + entityManager.persist(member) + entityManager.flush() + entityManager.clear() + } + } + + if (member.isAdmin()) { + loginAsAdmin() + } else { + loginAsUser() + } + resolveMemberId(member.id!!) + + return member + } + + private fun loginAsUser() { + every { + loginInterceptor.preHandle(any(), any(), any()) + } returns true + } + + private fun loginAsAdmin() { + every { + adminInterceptor.preHandle(any(), any(), any()) + } returns true + } + + private fun resolveMemberId(memberId: Long) { + every { + memberIdResolver.resolveArgument(any(), any(), any(), any()) + } returns memberId + } }