diff --git a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt index 71d9696d..94ff5c8e 100644 --- a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt @@ -4,114 +4,162 @@ import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.every -import io.mockk.just import io.mockk.mockk -import io.mockk.runs +import io.mockk.slot import roomescape.payment.exception.PaymentErrorCode import roomescape.payment.exception.PaymentException -import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository -import roomescape.payment.infrastructure.persistence.PaymentRepository +import roomescape.payment.implement.PaymentFinder +import roomescape.payment.implement.PaymentWriter +import roomescape.payment.infrastructure.client.PaymentApproveResponse +import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity import roomescape.payment.web.PaymentCancelRequest +import roomescape.payment.web.PaymentCancelResponse import roomescape.util.PaymentFixture -import roomescape.util.TsidFactory +import roomescape.util.ReservationFixture +import java.time.LocalDate +import java.time.LocalTime import java.time.OffsetDateTime +import java.time.ZoneOffset class PaymentServiceTest : FunSpec({ - val paymentRepository: PaymentRepository = mockk() - val canceledPaymentRepository: CanceledPaymentRepository = mockk() + val paymentFinder: PaymentFinder = mockk() + val paymentWriter: PaymentWriter = mockk() - val paymentService = PaymentService(TsidFactory, paymentRepository, canceledPaymentRepository) + val paymentService = PaymentService(paymentFinder, paymentWriter) - context("createCanceledPaymentByReservationId") { - val reservationId = 1L - test("reservationId로 paymentKey를 찾을 수 없으면 예외를 던진다.") { - every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null + context("createPayment") { + val approvedPaymentInfo = PaymentApproveResponse( + paymentKey = "paymentKey", + orderId = "orderId", + totalAmount = 1000L, + approvedAt = OffsetDateTime.now(), + ) + val reservation = ReservationFixture.create(id = 1L) - val exception = shouldThrow { - paymentService.createCanceledPaymentByReservationId(reservationId) + test("정상 응답") { + every { + paymentWriter.create( + paymentKey = approvedPaymentInfo.paymentKey, + orderId = approvedPaymentInfo.orderId, + totalAmount = approvedPaymentInfo.totalAmount, + approvedAt = approvedPaymentInfo.approvedAt, + reservation = reservation + ) + } returns PaymentFixture.create( + id = 1L, + orderId = approvedPaymentInfo.orderId, + paymentKey = approvedPaymentInfo.paymentKey, + totalAmount = approvedPaymentInfo.totalAmount, + approvedAt = approvedPaymentInfo.approvedAt, + reservation = reservation + ) + + val response = paymentService.createPayment(approvedPaymentInfo, reservation) + + assertSoftly(response) { + it.id shouldBe 1L + it.paymentKey shouldBe approvedPaymentInfo.paymentKey + it.reservationId shouldBe reservation.id } - exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } + } - context("reservationId로 paymentKey를 찾고난 후") { - val paymentKey = "test-payment-key" + context("createCanceledPayment(canceledPaymentInfo)") { + val canceledPaymentInfo = PaymentCancelResponse( + cancelStatus = "normal", + cancelReason = "고객 요청", + cancelAmount = 1000L, + canceledAt = OffsetDateTime.now(), + ) + val approvedAt = OffsetDateTime.now() + val paymentKey = "paymentKey" + + test("CanceledPaymentEntity를 응답") { + every { + paymentWriter.createCanceled( + cancelReason = canceledPaymentInfo.cancelReason, + cancelAmount = canceledPaymentInfo.cancelAmount, + canceledAt = canceledPaymentInfo.canceledAt, + approvedAt = approvedAt, + paymentKey = paymentKey + ) + } returns PaymentFixture.createCanceled( + id = 1L, + paymentKey = paymentKey, + cancelAmount = canceledPaymentInfo.cancelAmount + ) + + val response = paymentService.createCanceledPayment(canceledPaymentInfo, approvedAt, paymentKey) + + response.shouldBeInstanceOf() + response.paymentKey shouldBe paymentKey + } + } + + context("createCanceledPayment(reservationId)") { + val reservationId = 1L + + test("취소 사유를 '예약 취소'로 하여 PaymentCancelRequest를 응답") { + val payment = PaymentFixture.create(id = 1L, paymentKey = "paymentKey", totalAmount = 1000L) + every { + paymentFinder.findByReservationId(reservationId) + } returns payment + + val cancelReasonSlot = slot() every { - paymentRepository.findPaymentKeyByReservationId(reservationId) - } returns paymentKey + paymentWriter.createCanceled(payment, capture(cancelReasonSlot), any()) + } returns PaymentFixture.createCanceled( + id = 1L, + paymentKey = payment.paymentKey, + cancelAmount = payment.totalAmount + ) - test("해당 paymentKey로 paymentEntity를 찾을 수 없으면 예외를 던진다.") { - every { - paymentRepository.findByPaymentKey(paymentKey) - } returns null + val response = paymentService.createCanceledPayment(reservationId) - val exception = shouldThrow { - paymentService.createCanceledPaymentByReservationId(reservationId) - } - exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND - } + response.shouldBeInstanceOf() + cancelReasonSlot.captured shouldBe "예약 취소" + } - test("해당 paymentKey로 paymentEntity를 찾고, cancelPaymentEntity를 저장한다.") { - val paymentEntity = PaymentFixture.create(paymentKey = paymentKey) + test("결제 정보가 없으면 예외 응답") { + every { + paymentFinder.findByReservationId(reservationId) + } throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) - every { - paymentRepository.findByPaymentKey(paymentKey) - } returns paymentEntity.also { - every { - paymentRepository.delete(it) - } just runs - } - - every { - canceledPaymentRepository.save(any()) - } returns PaymentFixture.createCanceled( - id = 1L, - paymentKey = paymentKey, - cancelReason = "Test", - cancelAmount = paymentEntity.totalAmount, - ) - - val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId) - - assertSoftly(result) { - this.paymentKey shouldBe paymentKey - this.amount shouldBe paymentEntity.totalAmount - this.cancelReason shouldBe "Test" - } + shouldThrow { + paymentService.createCanceledPayment(reservationId) + }.also { + it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } } } context("updateCanceledTime") { - val paymentKey = "test-payment-key" - val canceledAt = OffsetDateTime.now() + val paymentKey = "paymentKey" + val canceledAt = OffsetDateTime.of(LocalDate.of(2025, 8, 5), LocalTime.of(10, 0), ZoneOffset.UTC) - test("paymentKey로 canceledPaymentEntity를 찾을 수 없으면 예외를 던진다.") { + test("정상 응답") { + val canceled = PaymentFixture.createCanceled(id = 1L) every { - canceledPaymentRepository.findByPaymentKey(paymentKey) - } returns null - - val exception = shouldThrow { - paymentService.updateCanceledTime(paymentKey, canceledAt) - } - exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND - } - - test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") { - val canceledPaymentEntity = PaymentFixture.createCanceled( - paymentKey = paymentKey, - canceledAt = canceledAt.minusMinutes(1) - ) - - every { - canceledPaymentRepository.findByPaymentKey(paymentKey) - } returns canceledPaymentEntity + paymentFinder.findCanceledByKey(paymentKey) + } returns canceled paymentService.updateCanceledTime(paymentKey, canceledAt) - assertSoftly(canceledPaymentEntity) { - this.canceledAt shouldBe canceledAt + canceled.canceledAt shouldBe canceledAt + } + + test("결제 취소 정보가 없으면 예외 응답") { + every { + paymentFinder.findCanceledByKey(paymentKey) + } throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) + + shouldThrow { + paymentService.updateCanceledTime(paymentKey, canceledAt) + }.also { + it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } } } diff --git a/src/test/kotlin/roomescape/payment/implement/PaymentFinderTest.kt b/src/test/kotlin/roomescape/payment/implement/PaymentFinderTest.kt new file mode 100644 index 00000000..b5fd6d10 --- /dev/null +++ b/src/test/kotlin/roomescape/payment/implement/PaymentFinderTest.kt @@ -0,0 +1,93 @@ +package roomescape.payment.implement + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException +import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository +import roomescape.payment.infrastructure.persistence.PaymentRepository + +class PaymentFinderTest : FunSpec({ + val paymentRepository: PaymentRepository = mockk() + val canceledPaymentRepository: CanceledPaymentRepository = mockk() + + val paymentFinder = PaymentFinder(paymentRepository, canceledPaymentRepository) + + context("existsPaymentByReservationId") { + val reservationId = 1L + test("결제 정보가 있으면 true를 반환한다.") { + every { + paymentRepository.existsByReservationId(reservationId) + } returns true + + paymentFinder.existsPaymentByReservationId(reservationId) shouldBe true + } + + test("결제 정보가 없으면 false를 반환한다.") { + every { + paymentRepository.existsByReservationId(reservationId) + } returns false + + paymentFinder.existsPaymentByReservationId(reservationId) shouldBe false + } + } + + context("findByReservationId") { + val reservationId = 1L + test("결제 정보를 조회한다.") { + every { + paymentRepository.findByReservationId(reservationId) + } returns mockk() + + paymentFinder.findByReservationId(reservationId) + + verify(exactly = 1) { + paymentRepository.findByReservationId(reservationId) + } + } + + test("결제 정보가 없으면 실패한다.") { + every { + paymentRepository.findByReservationId(reservationId) + } returns null + + shouldThrow { + paymentFinder.findByReservationId(reservationId) + }.also { + it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND + } + } + } + + context("findCanceledByKey") { + val paymentKey = "paymentKey" + + test("결제 취소 정보를 조회한다.") { + every { + canceledPaymentRepository.findByPaymentKey(paymentKey) + } returns mockk() + + paymentFinder.findCanceledByKey(paymentKey) + + verify(exactly = 1) { + canceledPaymentRepository.findByPaymentKey(paymentKey) + } + } + + test("결제 취소 정보가 없으면 실패한다.") { + every { + canceledPaymentRepository.findByPaymentKey(paymentKey) + } returns null + + shouldThrow { + paymentFinder.findCanceledByKey(paymentKey) + }.also { + it.errorCode shouldBe PaymentErrorCode.CANCELED_PAYMENT_NOT_FOUND + } + } + } +}) diff --git a/src/test/kotlin/roomescape/payment/implement/PaymentWriterTest.kt b/src/test/kotlin/roomescape/payment/implement/PaymentWriterTest.kt new file mode 100644 index 00000000..101b4fd2 --- /dev/null +++ b/src/test/kotlin/roomescape/payment/implement/PaymentWriterTest.kt @@ -0,0 +1,114 @@ +package roomescape.payment.implement + +import com.ninjasquad.springmockk.MockkClear +import com.ninjasquad.springmockk.clear +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.date.after +import io.kotest.matchers.shouldBe +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +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.reservation.infrastructure.persistence.ReservationEntity +import roomescape.util.PaymentFixture +import roomescape.util.ReservationFixture +import roomescape.util.TsidFactory +import java.time.OffsetDateTime + +class PaymentWriterTest : FunSpec({ + val paymentRepository: PaymentRepository = mockk() + val canceledPaymentRepository: CanceledPaymentRepository = mockk() + + val paymentWriter = PaymentWriter(paymentRepository, canceledPaymentRepository, TsidFactory) + + val paymentKey = "paymentKey" + val orderId = "orderId" + val totalAmount = 1000L + val approvedAt = OffsetDateTime.now() + + context("create") { + val reservation: ReservationEntity = ReservationFixture.create(id = 1L) + test("결제 정보를 저장한다.") { + val slot = slot() + every { + paymentRepository.save(capture(slot)) + } returns mockk() + + paymentWriter.create(paymentKey, orderId, totalAmount, approvedAt, reservation) + + verify(exactly = 1) { + paymentRepository.save(any()) + } + + slot.captured.also { + it.paymentKey shouldBe paymentKey + it.orderId shouldBe orderId + it.totalAmount shouldBe totalAmount + it.approvedAt shouldBe approvedAt + } + + paymentRepository.clear(MockkClear.AFTER) + } + } + + context("createCanceled") { + val cancelReason = "고객 요청" + val canceledAt = OffsetDateTime.now() + + afterTest { + clearMocks(canceledPaymentRepository) + } + + test("PaymentEntity를 받아 저장한다.") { + val payment: PaymentEntity = PaymentFixture.create(id = 1L) + val slot = slot() + + every { + canceledPaymentRepository.save(capture(slot)) + } returns mockk() + + paymentWriter.createCanceled(payment, cancelReason, canceledAt) + + verify(exactly = 1) { + canceledPaymentRepository.save(any()) + } + + slot.captured.also { + it.paymentKey shouldBe payment.paymentKey + it.cancelAmount shouldBe payment.totalAmount + it.approvedAt shouldBe payment.approvedAt + } + } + + test("취소 정보를 받아 저장한다.") { + val slot = slot() + + every { + canceledPaymentRepository.save(capture(slot)) + } returns mockk() + + paymentWriter.createCanceled( + cancelReason = cancelReason, + cancelAmount = totalAmount, + canceledAt = canceledAt, + approvedAt = approvedAt, + paymentKey = paymentKey + ) + + verify(exactly = 1) { + canceledPaymentRepository.save(any()) + } + + slot.captured.also { + it.paymentKey shouldBe paymentKey + it.cancelAmount shouldBe totalAmount + it.approvedAt shouldBe approvedAt + } + } + } +}) diff --git a/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt b/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt index 51979b53..5ed4652d 100644 --- a/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt +++ b/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt @@ -39,28 +39,6 @@ class PaymentRepositoryTest( } } - context("findPaymentKeyByReservationId") { - lateinit var paymentKey: String - - beforeTest { - reservation = setupReservation() - paymentKey = PaymentFixture.create(reservation = reservation) - .also { paymentRepository.save(it) } - .paymentKey - } - - test("정상 반환") { - paymentRepository.findPaymentKeyByReservationId(reservation.id!!) - ?.let { it shouldBe paymentKey } - ?: throw AssertionError("Unexpected null value") - } - - test("null 반환") { - paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1) - .also { it shouldBe null } - } - } - context("findByPaymentKey") { lateinit var payment: PaymentEntity diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt index c8985f79..c442f624 100644 --- a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt @@ -13,8 +13,8 @@ import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.toCreateResponse import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.reservation.web.ReservationCreateResponse import roomescape.reservation.web.ReservationCreateWithPaymentRequest -import roomescape.reservation.web.ReservationRetrieveResponse import roomescape.util.* class ReservationWithPaymentServiceTest : FunSpec({ @@ -55,9 +55,9 @@ class ReservationWithPaymentServiceTest : FunSpec({ paymentService.createPayment(paymentApproveResponse, reservationEntity) } returns paymentEntity.toCreateResponse() - val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment( + val result: ReservationCreateResponse = reservationWithPaymentService.createReservationAndPayment( request = reservationCreateWithPaymentRequest, - paymentInfo = paymentApproveResponse, + approvedPaymentInfo = paymentApproveResponse, memberId = memberId ) @@ -81,7 +81,7 @@ class ReservationWithPaymentServiceTest : FunSpec({ ) every { - paymentService.createCanceledPaymentByReservationId(reservationEntity.id!!) + paymentService.createCanceledPayment(reservationEntity.id!!) } returns paymentCancelRequest every { @@ -100,7 +100,7 @@ class ReservationWithPaymentServiceTest : FunSpec({ context("isNotPaidReservation") { test("결제된 예약이면 true를 반환한다.") { every { - paymentService.isReservationPaid(reservationEntity.id!!) + paymentService.existsByReservationId(reservationEntity.id!!) } returns false val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!) @@ -110,7 +110,7 @@ class ReservationWithPaymentServiceTest : FunSpec({ test("결제되지 않은 예약이면 false를 반환한다.") { every { - paymentService.isReservationPaid(reservationEntity.id!!) + paymentService.existsByReservationId(reservationEntity.id!!) } returns true val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)