From b636ac926ef866af68d301dd139444e4de50f7da Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 16 Oct 2025 15:29:49 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20OrderServ?= =?UTF-8?q?ice=EC=9D=98=20=ED=99=95=EC=A0=95=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sangdol/roomescape/order/OrderApiTest.kt | 150 ++++++------------ .../roomescape/order/OrderConcurrencyTest.kt | 14 +- .../roomescape/supports/DummyInitializer.kt | 21 +++ 3 files changed, 71 insertions(+), 114 deletions(-) diff --git a/service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt index 5b0fd1be..acd2937a 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/order/OrderApiTest.kt @@ -1,46 +1,38 @@ package com.sangdol.roomescape.order -import com.ninjasquad.springmockk.SpykBean +import com.ninjasquad.springmockk.MockkBean 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.business.event.PaymentEvent +import com.sangdol.roomescape.payment.business.event.PaymentEventListener import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse +import com.sangdol.roomescape.payment.exception.ExternalPaymentException 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.payment.infrastructure.client.TosspayClient +import com.sangdol.roomescape.reservation.business.event.ReservationConfirmEvent +import com.sangdol.roomescape.reservation.business.event.ReservationEventListener 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 io.mockk.* 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, + @MockkBean(relaxed = true) private val paymentClient: TosspayClient, + @MockkBean(relaxed = true) private val reservationEventListener: ReservationEventListener, + @MockkBean(relaxed = true) private val paymentEventListener: PaymentEventListener, 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 @@ -82,39 +74,52 @@ class OrderApiTest( } test("정상 응답") { - val reservation = dummyInitializer.createPendingReservation(user) + val reservationId = dummyInitializer.createPendingReservation(user).id + + val reservationConfirmEventSlot = slot() + val paymentEventSlot = slot() every { - paymentService.requestConfirm(paymentRequest) + paymentClient.confirm(paymentRequest.paymentKey, paymentRequest.orderId, paymentRequest.amount) } returns expectedPaymentResponse + every { + paymentEventListener.handlePaymentEvent(capture(paymentEventSlot)) + } just runs + + every { + reservationEventListener.handleReservationConfirmEvent(capture(reservationConfirmEventSlot)) + } just runs + runTest( token = token, using = { body(paymentRequest) }, on = { - post("/orders/${reservation.id}/confirm") + post("/orders/${reservationId}/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() + verify(exactly = 1) { + paymentEventListener.handlePaymentEvent(any()) + }.also { + assertSoftly(paymentEventSlot.captured) { + this.paymentKey shouldBe expectedPaymentResponse.paymentKey + this.reservationId shouldBe reservationId + } } - paymentAttemptRepository.isSuccessAttemptExists(reservation.id).shouldBeTrue() + verify(exactly = 1) { + reservationEventListener.handleReservationConfirmEvent(any()) + }.also { + assertSoftly(reservationConfirmEventSlot.captured) { + this.reservationId shouldBe reservationId + } + } } context("검증 과정에서의 실패 응답") { @@ -128,24 +133,6 @@ class OrderApiTest( ) } - 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) @@ -223,68 +210,23 @@ class OrderApiTest( } context("결제 과정에서의 실패 응답.") { - test("결제에 실패해도 검증을 통과하면 예약을 ${ReservationStatus.PAYMENT_IN_PROGRESS} 상태로 바꾸고, 결제 시도 이력을 기록한다.") { - val reservation = dummyInitializer.createPendingReservation(user) + test("결제에 실패해도 검증을 통과하면 예약을 ${ReservationStatus.PAYMENT_IN_PROGRESS} 상태가 된다.") { + val reservationId = dummyInitializer.createPendingReservation(user).id every { - paymentService.requestConfirm(paymentRequest) - } throws PaymentException(PaymentErrorCode.PAYMENT_CLIENT_ERROR) + paymentClient.confirm(paymentRequest.paymentKey, paymentRequest.orderId, paymentRequest.amount) + } throws ExternalPaymentException(400, "INVALID_REQUEST", "잘못 요청함") runExceptionTest( token = token, method = HttpMethod.POST, - endpoint = "/orders/${reservation.id}/confirm", + endpoint = "/orders/${reservationId}/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 + assertSoftly(reservationRepository.findByIdOrNull(reservationId)!!) { + this.status shouldBe ReservationStatus.PAYMENT_IN_PROGRESS } } } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/order/OrderConcurrencyTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/order/OrderConcurrencyTest.kt index 768323d7..6a72317a 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/order/OrderConcurrencyTest.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/order/OrderConcurrencyTest.kt @@ -18,6 +18,7 @@ import com.sangdol.roomescape.supports.ReservationFixture import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.mockk.every import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -69,7 +70,7 @@ class OrderConcurrencyTest( test("결제 요청 직후 시작되는 예약 만료 배치는, 해당 예약이 만료되었더라도 적용되지 않는다.") { every { - paymentService.requestConfirm(paymentConfirmRequest) + paymentService.requestConfirm(reservation.id, paymentConfirmRequest) } returns paymentGatewayResponse withContext(Dispatchers.IO) { @@ -87,18 +88,13 @@ class OrderConcurrencyTest( } assertSoftly(reservationRepository.findByIdOrNull(reservation.id)!!) { - this.status shouldBe ReservationStatus.CONFIRMED - } - - assertSoftly(scheduleRepository.findByIdOrNull(schedule.id)!!) { - this.status shouldBe ScheduleStatus.RESERVED - this.holdExpiredAt shouldBe null + this.status shouldNotBe ReservationStatus.EXPIRED } } test("결제 요청 직전에 예약 만료 배치가 시작하면, 결제 요청은 실패한다.") { every { - paymentService.requestConfirm(paymentConfirmRequest) + paymentService.requestConfirm(reservation.id, paymentConfirmRequest) } returns paymentGatewayResponse withContext(Dispatchers.IO) { @@ -113,8 +109,6 @@ class OrderConcurrencyTest( async { assertThrows { orderService.confirm(reservation.id, paymentConfirmRequest) - }.also { - it.trial shouldBe 0 } } } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt b/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt index 324d9f29..d099c9f8 100644 --- a/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt +++ b/service/src/test/kotlin/com/sangdol/roomescape/supports/DummyInitializer.kt @@ -157,6 +157,27 @@ class DummyInitializer( } } + fun createExpiredOrCanceledReservation( + user: UserEntity, + status: ReservationStatus, + storeId: Long = IDGenerator.create(), + themeRequest: ThemeCreateRequest = ThemeFixture.createRequest, + scheduleRequest: ScheduleCreateRequest = ScheduleFixture.createRequest, + reservationRequest: PendingReservationCreateRequest = ReservationFixture.pendingCreateRequest, + ): ReservationEntity { + return createPendingReservation(user, storeId, themeRequest, scheduleRequest, reservationRequest).apply { + this.status = status + }.also { + reservationRepository.save(it) + + scheduleRepository.findByIdOrNull(it.scheduleId)?.let { schedule -> + schedule.status = ScheduleStatus.AVAILABLE + schedule.holdExpiredAt = null + scheduleRepository.save(schedule) + } + } + } + fun createPayment( reservationId: Long, request: PaymentConfirmRequest = PaymentFixture.confirmRequest,