From 7898a931821fae2358c3fa7ada814367cdffbbc0 Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 1 Oct 2025 12:42:39 +0900 Subject: [PATCH] =?UTF-8?q?test:=20tosspay-mock=20=EB=AA=A8=EB=93=88=20API?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sangdol/tosspaymock/TosspayApiTest.kt | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/TosspayApiTest.kt diff --git a/tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/TosspayApiTest.kt b/tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/TosspayApiTest.kt new file mode 100644 index 00000000..86e9fc7c --- /dev/null +++ b/tosspay-mock/src/test/kotlin/com/sangdol/tosspaymock/TosspayApiTest.kt @@ -0,0 +1,188 @@ +package com.sangdol.tosspaymock + +import com.sangdol.common.persistence.IDGenerator +import com.sangdol.tosspaymock.exception.code.TosspayCancelErrorCode +import com.sangdol.tosspaymock.exception.code.TosspayConfirmErrorCode +import com.sangdol.tosspaymock.infrastructure.persistence.OrderAmountEntity +import com.sangdol.tosspaymock.infrastructure.persistence.OrderAmountRepository +import com.sangdol.tosspaymock.web.dto.PaymentCancelRequest +import com.sangdol.tosspaymock.web.dto.PaymentConfirmRequest +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.restassured.RestAssured +import io.restassured.module.kotlin.extensions.Extract +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.CoreMatchers +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class TosspayApiTest( + @LocalServerPort private val port: Int, + private val orderAmountRepository: OrderAmountRepository, + private val idGenerator: IDGenerator +) : FunSpec({ + + beforeSpec { + RestAssured.port = port + } + + afterTest { + orderAmountRepository.deleteAll() + } + + val authorizationKey = "dGVzdF9nc2tfZG9jc19PYVB6OEw1S2RtUVhrelJ6M3k0N0JNdzY6" + + val paymentConfirmRequest = PaymentConfirmRequest( + paymentKey = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1", + orderId = "MC4wODU4ODQwMzg4NDk0", + amount = 100_000, + ) + + val paymentCancelRequest = PaymentCancelRequest( + cancelAmount = paymentConfirmRequest.amount, + cancelReason = "그냥.." + ) + + context("결제 승인") { + val paymentConfirmRequest = PaymentConfirmRequest( + paymentKey = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1", + orderId = "MC4wODU4ODQwMzg4NDk0", + amount = 100_000, + ) + + test("Basic Authorization 헤더가 없으면 실패한다.") { + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + body(paymentConfirmRequest) + } When { + post("/v1/payments/confirm") + } Then { + statusCode(HttpStatus.BAD_REQUEST.value()) + body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.INVALID_API_KEY.name)) + } + } + + test("Basic Authorization 헤더가 Base64 형식이 아니면 실패한다.") { + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Basic hello-world") + body(paymentConfirmRequest) + } When { + post("/v1/payments/confirm") + } Then { + statusCode(HttpStatus.UNAUTHORIZED.value()) + body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.UNAUTHORIZED_KEY.name)) + } + } + + test("임의의 결제 정보를 반환하며, 금액은 별도로 저장한다.") { + val paymentKey = Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Basic $authorizationKey") + body(paymentConfirmRequest) + } When { + post("/v1/payments/confirm") + } Then { + statusCode(HttpStatus.OK.value()) + } Extract { + path("paymentKey") + } + + paymentKey shouldBe paymentConfirmRequest.paymentKey + orderAmountRepository.findByPaymentKey(paymentKey).shouldNotBeNull() + } + } + + context("결제 취소") { + test("Basic Authorization 헤더가 없으면 실패한다.") { + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + body(paymentCancelRequest) + } When { + post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel") + } Then { + statusCode(HttpStatus.BAD_REQUEST.value()) + body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.INVALID_API_KEY.name)) + } + } + + test("Basic Authorization 헤더가 Base64 형식이 아니면 실패한다.") { + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Basic hello-world") + body(paymentCancelRequest) + } When { + post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel") + } Then { + statusCode(HttpStatus.UNAUTHORIZED.value()) + body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.UNAUTHORIZED_KEY.name)) + } + } + + context("정상 응답") { + lateinit var orderAmount: OrderAmountEntity + + beforeTest { + orderAmount = OrderAmountEntity( + id = idGenerator.create(), + paymentKey = paymentConfirmRequest.paymentKey, + approvedAmount = (paymentConfirmRequest.amount - 1000), + easypayDiscountAmount = 1000, + cardDiscountAmount = 0, + transferDiscountAmount = 0 + ).also { + orderAmountRepository.saveAndFlush(it) + } + } + + test("요청에 cancelAmount를 포함하지 않으면 전체 금액을 취소한다.") { + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Basic $authorizationKey") + body(PaymentCancelRequest(cancelReason = "그냥!")) + } When { + post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel") + } Then { + statusCode(HttpStatus.OK.value()) + body("paymentKey", CoreMatchers.equalTo(paymentConfirmRequest.paymentKey)) + body("cancels.easyPayDiscountAmount", CoreMatchers.equalTo(orderAmount.easypayDiscountAmount)) + body("cancels.cancelAmount", CoreMatchers.equalTo(orderAmount.totalAmount())) + } + } + + test("이전 결제 정보의 할인 금액을 가져온 뒤 반환한다.") { + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Basic $authorizationKey") + body(paymentCancelRequest) + } When { + post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel") + } Then { + statusCode(HttpStatus.OK.value()) + body("paymentKey", CoreMatchers.equalTo(paymentConfirmRequest.paymentKey)) + body("cancels.easyPayDiscountAmount", CoreMatchers.equalTo(orderAmount.easypayDiscountAmount)) + body("cancels.cancelAmount", CoreMatchers.equalTo(paymentCancelRequest.cancelAmount)) + } + } + } + + test("이전 결제 정보가 없으면 실패한다.") { + Given { + contentType(MediaType.APPLICATION_JSON_VALUE) + header("Authorization", "Basic $authorizationKey") + body(paymentCancelRequest) + } When { + post("/v1/payments/notExistPaymentKey/cancel") + } Then { + statusCode(HttpStatus.NOT_FOUND.value()) + body("code", CoreMatchers.equalTo(TosspayCancelErrorCode.NOT_FOUND_PAYMENT.name)) + } + } + } +})