From 876725e0e17b79ba7ac9237613bf5727e020df9d Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 9 Oct 2025 15:13:19 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EA=B2=B0=EC=A0=9C=20&=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=ED=99=95=EC=A0=95=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sangdol/roomescape/order/OrderApiTest.kt | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt diff --git a/service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt new file mode 100644 index 00000000..5b0fd1be --- /dev/null +++ b/service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt @@ -0,0 +1,293 @@ +package com.sangdol.roomescape.order + +import com.ninjasquad.springmockk.SpykBean +import com.sangdol.common.utils.KoreaDate +import com.sangdol.common.utils.KoreaTime +import com.sangdol.roomescape.auth.exception.AuthErrorCode +import com.sangdol.roomescape.order.exception.OrderErrorCode +import com.sangdol.roomescape.order.infrastructure.persistence.AttemptResult +import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptEntity +import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptRepository +import com.sangdol.roomescape.order.infrastructure.persistence.PostOrderTaskRepository +import com.sangdol.roomescape.payment.business.PaymentService +import com.sangdol.roomescape.payment.business.domain.PaymentMethod +import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest +import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse +import com.sangdol.roomescape.payment.exception.PaymentErrorCode +import com.sangdol.roomescape.payment.exception.PaymentException +import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentDetailRepository +import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository +import com.sangdol.roomescape.reservation.exception.ReservationErrorCode +import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository +import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus +import com.sangdol.roomescape.supports.* +import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.mockk.every +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus + +class OrderApiTest( + @SpykBean private val paymentService: PaymentService, + private val paymentAttemptRepository: PaymentAttemptRepository, + private val reservationRepository: ReservationRepository, + private val postOrderTaskRepository: PostOrderTaskRepository, + private val scheduleRepository: ScheduleRepository, + private val paymentRepository: PaymentRepository, + private val paymentDetailRepository: PaymentDetailRepository +) : FunSpecSpringbootTest() { + + val paymentRequest: PaymentConfirmRequest = PaymentFixture.confirmRequest + val expectedPaymentResponse: PaymentGatewayResponse = PaymentFixture.confirmResponse( + paymentKey = paymentRequest.paymentKey, + orderId = paymentRequest.orderId, + amount = paymentRequest.amount, + method = PaymentMethod.CARD + ) + + init { + context("결제 및 예약을 확정한다.") { + lateinit var user: UserEntity + lateinit var token: String + + beforeTest { + val loginResult = testAuthUtil.defaultUserLogin() + user = loginResult.first + token = loginResult.second + } + + context("권한이 없으면 접근할 수 없다.") { + test("비회원") { + runExceptionTest( + method = HttpMethod.POST, + endpoint = "/orders/${INVALID_PK}/confirm", + expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND + ) + } + + test("관리자") { + runExceptionTest( + token = testAuthUtil.defaultHqAdminLogin().second, + method = HttpMethod.POST, + endpoint = "/orders/${INVALID_PK}/confirm", + expectedErrorCode = AuthErrorCode.ACCESS_DENIED + ) + } + } + + test("정상 응답") { + val reservation = dummyInitializer.createPendingReservation(user) + + every { + paymentService.requestConfirm(paymentRequest) + } returns expectedPaymentResponse + + runTest( + token = token, + using = { + body(paymentRequest) + }, + on = { + post("/orders/${reservation.id}/confirm") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ) + + assertSoftly(scheduleRepository.findByIdOrNull(reservation.scheduleId)!!) { + this.status shouldBe ScheduleStatus.RESERVED + this.holdExpiredAt shouldBe null + } + reservationRepository.findByIdOrNull(reservation.id)!!.status shouldBe ReservationStatus.CONFIRMED + + assertSoftly(paymentRepository.findByReservationId(reservation.id)) { + this.shouldNotBeNull() + this.status shouldBe expectedPaymentResponse.status + + paymentDetailRepository.findByPaymentId(this.id)!!.shouldNotBeNull() + } + + paymentAttemptRepository.isSuccessAttemptExists(reservation.id).shouldBeTrue() + } + + context("검증 과정에서의 실패 응답") { + test("예약이 없으면 실패한다.") { + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${INVALID_PK}/confirm", + requestBody = paymentRequest, + expectedErrorCode = ReservationErrorCode.RESERVATION_NOT_FOUND + ) + } + + test("이미 결제가 완료된 예약이면 실패한다.") { + val reservation = dummyInitializer.createPendingReservation(user) + + paymentAttemptRepository.save(PaymentAttemptEntity( + id = IDGenerator.create(), + reservationId = reservation.id, + result = AttemptResult.SUCCESS + )) + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${reservation.id}/confirm", + requestBody = paymentRequest, + expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE + ) + } + + test("이미 확정된 예약이면 실패한다.") { + val reservation = dummyInitializer.createConfirmReservation(user) + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${reservation.id}/confirm", + requestBody = paymentRequest, + expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE + ) + } + + test("만료된 예약이면 실패한다.") { + val reservation = dummyInitializer.createExpiredOrCanceledReservation(user, ReservationStatus.EXPIRED) + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${reservation.id}/confirm", + requestBody = paymentRequest, + expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE + ) + } + + test("취소된 예약이면 실패한다.") { + val reservation = dummyInitializer.createExpiredOrCanceledReservation(user, ReservationStatus.CANCELED) + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${reservation.id}/confirm", + requestBody = paymentRequest, + expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE + ) + } + + test("일정이 HOLD 상태가 아니라면 실패한다.") { + val schedule = dummyInitializer.createSchedule(status = ScheduleStatus.AVAILABLE) + val reservation = dummyInitializer.createPendingReservation( + user = user, + reservationRequest = ReservationFixture.pendingCreateRequest.copy(scheduleId = schedule.id) + ) + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${reservation.id}/confirm", + requestBody = paymentRequest, + expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE + ) + } + + test("일정의 시작 시간이 현재 시간 이전이면 실패한다.") { + val schedule = dummyInitializer.createSchedule( + status = ScheduleStatus.HOLD, + request = ScheduleFixture.createRequest.copy( + date = KoreaDate.today(), + time = KoreaTime.now() + ) + ) + + val reservation = dummyInitializer.createPendingReservation( + user = user, + reservationRequest = ReservationFixture.pendingCreateRequest.copy(scheduleId = schedule.id) + ) + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${reservation.id}/confirm", + requestBody = paymentRequest, + expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE + ) + } + } + + context("결제 과정에서의 실패 응답.") { + test("결제에 실패해도 검증을 통과하면 예약을 ${ReservationStatus.PAYMENT_IN_PROGRESS} 상태로 바꾸고, 결제 시도 이력을 기록한다.") { + val reservation = dummyInitializer.createPendingReservation(user) + + every { + paymentService.requestConfirm(paymentRequest) + } throws PaymentException(PaymentErrorCode.PAYMENT_CLIENT_ERROR) + + runExceptionTest( + token = token, + method = HttpMethod.POST, + endpoint = "/orders/${reservation.id}/confirm", + requestBody = paymentRequest, + expectedErrorCode = PaymentErrorCode.PAYMENT_CLIENT_ERROR + ).also { + it.extract().path("trial") shouldBe 0 + } + + assertSoftly(reservationRepository.findByIdOrNull(reservation.id)!!) { + this.status shouldBe ReservationStatus.PAYMENT_IN_PROGRESS + } + + val paymentAttempt = paymentAttemptRepository.findAll().first { it.reservationId == reservation.id } + assertSoftly(paymentAttempt) { + it.shouldNotBeNull() + it.result shouldBe AttemptResult.FAILED + it.errorCode shouldBe PaymentErrorCode.PAYMENT_CLIENT_ERROR.name + } + } + } + + context("결제 성공 이후 실패 응답.") { + test("결제 이력 저장 과정에서 예외가 발생하면 해당 작업을 저장하며, 사용자는 정상 응답을 받는다.") { + val reservation = dummyInitializer.createPendingReservation(user) + + every { + paymentService.requestConfirm(paymentRequest) + } returns expectedPaymentResponse + + every { + paymentService.savePayment(reservation.id, expectedPaymentResponse) + } throws RuntimeException("결제 저장 실패!") + + runTest( + token = token, + using = { + body(paymentRequest) + }, + on = { + post("/orders/${reservation.id}/confirm") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ) + + paymentAttemptRepository.isSuccessAttemptExists(reservation.id).shouldBeTrue() + + val postOrderTask = postOrderTaskRepository.findAll().first { it.reservationId == reservation.id } + assertSoftly(postOrderTask) { + it.shouldNotBeNull() + it.paymentKey shouldBe paymentRequest.paymentKey + it.trial shouldBe 1 + } + } + } + } + } +}