generated from pricelees/issue-pr-template
refactor: TossPaymentClient 코틀린 마이그레이션 및 테스트
This commit is contained in:
parent
6fdc9767c1
commit
313d9850d1
@ -1,95 +1,113 @@
|
||||
package roomescape.payment.infrastructure.client;
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.http.HttpRequest
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.HttpStatusCode
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.client.RestClient
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import java.io.IOException
|
||||
import java.util.Map
|
||||
|
||||
@Component
|
||||
public class TossPaymentClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TossPaymentClient.class);
|
||||
|
||||
private final RestClient tossPaymentClient;
|
||||
|
||||
public TossPaymentClient(RestClient.Builder tossPaymentClientBuilder) {
|
||||
this.tossPaymentClient = tossPaymentClientBuilder.build();
|
||||
class TossPaymentClient(
|
||||
private val log: KLogger = KotlinLogging.logger {},
|
||||
tossPaymentClientBuilder: RestClient.Builder
|
||||
) {
|
||||
companion object {
|
||||
private const val CONFIRM_URL: String = "/v1/payments/confirm"
|
||||
private const val CANCEL_URL: String = "/v1/payments/{paymentKey}/cancel"
|
||||
}
|
||||
|
||||
public PaymentApprove.Response confirmPayment(PaymentApprove.Request paymentRequest) {
|
||||
logPaymentInfo(paymentRequest);
|
||||
private val tossPaymentClient: RestClient = tossPaymentClientBuilder.build()
|
||||
|
||||
fun confirmPayment(paymentRequest: PaymentApprove.Request): PaymentApprove.Response {
|
||||
logPaymentInfo(paymentRequest)
|
||||
|
||||
return tossPaymentClient.post()
|
||||
.uri("/v1/payments/confirm")
|
||||
.uri(CONFIRM_URL)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(paymentRequest)
|
||||
.retrieve()
|
||||
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||
(req, res) -> handlePaymentError(res))
|
||||
.body(PaymentApprove.Response.class);
|
||||
.onStatus(
|
||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
||||
)
|
||||
.body(PaymentApprove.Response::class.java)
|
||||
?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
public PaymentCancel.Response cancelPayment(PaymentCancel.Request cancelRequest) {
|
||||
logPaymentCancelInfo(cancelRequest);
|
||||
Map<String, String> param = Map.of("cancelReason", cancelRequest.cancelReason);
|
||||
fun cancelPayment(cancelRequest: PaymentCancel.Request): PaymentCancel.Response {
|
||||
logPaymentCancelInfo(cancelRequest)
|
||||
val param = Map.of<String, String>("cancelReason", cancelRequest.cancelReason)
|
||||
|
||||
return tossPaymentClient.post()
|
||||
.uri("/v1/payments/{paymentKey}/cancel", cancelRequest.paymentKey)
|
||||
.uri(CANCEL_URL, cancelRequest.paymentKey)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(param)
|
||||
.retrieve()
|
||||
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||
(req, res) -> handlePaymentError(res))
|
||||
.body(PaymentCancel.Response.class);
|
||||
.onStatus(
|
||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
||||
)
|
||||
.body(PaymentCancel.Response::class.java)
|
||||
?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
private void logPaymentInfo(PaymentApprove.Request paymentRequest) {
|
||||
log.info("결제 승인 요청: paymentKey={}, orderId={}, amount={}, paymentType={}",
|
||||
paymentRequest.paymentKey, paymentRequest.orderId, paymentRequest.amount,
|
||||
paymentRequest.paymentType);
|
||||
private fun logPaymentInfo(paymentRequest: PaymentApprove.Request) {
|
||||
log.info {
|
||||
"결제 승인 요청: paymentKey=${paymentRequest.paymentKey}, orderId=${paymentRequest.orderId}, " +
|
||||
"amount=${paymentRequest.amount}, paymentType=${paymentRequest.paymentType}"
|
||||
}
|
||||
}
|
||||
|
||||
private void logPaymentCancelInfo(PaymentCancel.Request cancelRequest) {
|
||||
log.info("결제 취소 요청: paymentKey={}, amount={}, cancelReason={}",
|
||||
cancelRequest.paymentKey, cancelRequest.amount, cancelRequest.cancelReason);
|
||||
private fun logPaymentCancelInfo(cancelRequest: PaymentCancel.Request) {
|
||||
log.info {
|
||||
"결제 취소 요청: paymentKey=${cancelRequest.paymentKey}, amount=${cancelRequest.amount}, " +
|
||||
"cancelReason=${cancelRequest.cancelReason}"
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePaymentError(ClientHttpResponse res)
|
||||
throws IOException {
|
||||
HttpStatusCode statusCode = res.getStatusCode();
|
||||
ErrorType errorType = getErrorTypeByStatusCode(statusCode);
|
||||
TossPaymentErrorResponse errorResponse = getErrorResponse(res);
|
||||
@Throws(IOException::class)
|
||||
private fun handlePaymentError(
|
||||
res: ClientHttpResponse
|
||||
): Nothing {
|
||||
val statusCode = res.statusCode
|
||||
val errorType = getErrorTypeByStatusCode(statusCode)
|
||||
val errorResponse = getErrorResponse(res)
|
||||
|
||||
throw new RoomescapeException(errorType,
|
||||
String.format("[ErrorCode = %s, ErrorMessage = %s]", errorResponse.code, errorResponse.message),
|
||||
statusCode);
|
||||
throw RoomescapeException(
|
||||
errorType,
|
||||
"[ErrorCode = ${errorResponse.code}, ErrorMessage = ${errorResponse.message}]",
|
||||
statusCode
|
||||
)
|
||||
}
|
||||
|
||||
private TossPaymentErrorResponse getErrorResponse(ClientHttpResponse res) throws IOException {
|
||||
InputStream body = res.getBody();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
TossPaymentErrorResponse errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse.class);
|
||||
body.close();
|
||||
return errorResponse;
|
||||
@Throws(IOException::class)
|
||||
private fun getErrorResponse(
|
||||
res: ClientHttpResponse
|
||||
): TossPaymentErrorResponse {
|
||||
val body = res.body
|
||||
val objectMapper = ObjectMapper()
|
||||
val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java)
|
||||
body.close()
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
private ErrorType getErrorTypeByStatusCode(HttpStatusCode statusCode) {
|
||||
if (statusCode.is4xxClientError()) {
|
||||
return ErrorType.PAYMENT_ERROR;
|
||||
private fun getErrorTypeByStatusCode(
|
||||
statusCode: HttpStatusCode
|
||||
): ErrorType {
|
||||
if (statusCode.is4xxClientError) {
|
||||
return ErrorType.PAYMENT_ERROR
|
||||
}
|
||||
return ErrorType.PAYMENT_SERVER_ERROR;
|
||||
return ErrorType.PAYMENT_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
package roomescape.payment.infrastructure.client;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.*;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.payment.SampleTossPaymentConst;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
|
||||
@RestClientTest(TossPaymentClient.class)
|
||||
class TossPaymentClientTest {
|
||||
|
||||
@Autowired
|
||||
private TossPaymentClient tossPaymentClient;
|
||||
|
||||
@Autowired
|
||||
private MockRestServiceServer mockServer;
|
||||
|
||||
@Test
|
||||
@DisplayName("결제를 승인한다.")
|
||||
void confirmPayment() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/confirm"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.paymentRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.OK)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.confirmJson));
|
||||
|
||||
// when
|
||||
PaymentApprove.Request paymentRequest = SampleTossPaymentConst.paymentRequest;
|
||||
PaymentApprove.Response paymentResponse = tossPaymentClient.confirmPayment(paymentRequest);
|
||||
|
||||
// then
|
||||
assertThat(paymentResponse.paymentKey).isEqualTo(paymentRequest.paymentKey);
|
||||
assertThat(paymentResponse.orderId).isEqualTo(paymentRequest.orderId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제를 취소한다.")
|
||||
void cancelPayment() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.cancelRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.OK)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.cancelJson));
|
||||
|
||||
// when
|
||||
PaymentCancel.Request cancelRequest = SampleTossPaymentConst.cancelRequest;
|
||||
PaymentCancel.Response paymentCancelResponse = tossPaymentClient.cancelPayment(cancelRequest);
|
||||
|
||||
// then
|
||||
assertThat(paymentCancelResponse.cancelStatus).isEqualTo("DONE");
|
||||
assertThat(paymentCancelResponse.cancelReason).isEqualTo(cancelRequest.cancelReason);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 승인 중 400 에러가 발생한다.")
|
||||
void confirmPaymentWithError() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/confirm"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.paymentRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson));
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> tossPaymentClient.confirmPayment(SampleTossPaymentConst.paymentRequest))
|
||||
.isInstanceOf(RoomescapeException.class)
|
||||
.hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_ERROR)
|
||||
.hasFieldOrPropertyWithValue("invalidValue",
|
||||
"[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")
|
||||
.hasFieldOrPropertyWithValue("httpStatus", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 취소 중 500 에러가 발생한다.")
|
||||
void cancelPaymentWithError() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.cancelRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson));
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> tossPaymentClient.cancelPayment(SampleTossPaymentConst.cancelRequest))
|
||||
.isInstanceOf(RoomescapeException.class)
|
||||
.hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_SERVER_ERROR)
|
||||
.hasFieldOrPropertyWithValue("invalidValue",
|
||||
"[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")
|
||||
.hasFieldOrPropertyWithValue("httpStatus", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.client.MockRestServiceServer
|
||||
import org.springframework.test.web.client.ResponseActions
|
||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.payment.SampleTossPaymentConst
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
|
||||
@RestClientTest(TossPaymentClient::class)
|
||||
class TossPaymentClientTest(
|
||||
@Autowired val client: TossPaymentClient,
|
||||
@Autowired val mockServer: MockRestServiceServer
|
||||
) : FunSpec() {
|
||||
|
||||
init {
|
||||
context("결제 승인 요청") {
|
||||
fun commonAction(): ResponseActions = mockServer.expect {
|
||||
requestTo("/v1/payments/confirm")
|
||||
}.andExpect {
|
||||
method(HttpMethod.POST)
|
||||
}.andExpect {
|
||||
content().contentType(MediaType.APPLICATION_JSON)
|
||||
}.andExpect {
|
||||
content().json(SampleTossPaymentConst.paymentRequestJson)
|
||||
}
|
||||
|
||||
test("성공 응답") {
|
||||
commonAction().andRespond {
|
||||
withSuccess()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.confirmJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
||||
val paymentResponse: PaymentApprove.Response = client.confirmPayment(paymentRequest)
|
||||
|
||||
assertSoftly(paymentResponse) {
|
||||
this.paymentKey shouldBe paymentRequest.paymentKey
|
||||
this.orderId shouldBe paymentRequest.orderId
|
||||
this.totalAmount shouldBe paymentRequest.amount
|
||||
}
|
||||
}
|
||||
|
||||
test("400 에러 발생") {
|
||||
commonAction().andRespond {
|
||||
withStatus(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
||||
|
||||
// then
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
client.confirmPayment(paymentRequest)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.PAYMENT_ERROR
|
||||
this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]"
|
||||
this.httpStatus shouldBe HttpStatus.BAD_REQUEST
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
context("결제 취소 요청") {
|
||||
fun commonAction(): ResponseActions = mockServer.expect {
|
||||
requestTo("/v1/payments/${SampleTossPaymentConst.paymentKey}/cancel")
|
||||
}.andExpect {
|
||||
method(HttpMethod.POST)
|
||||
}.andExpect {
|
||||
content().contentType(MediaType.APPLICATION_JSON)
|
||||
}.andExpect {
|
||||
content().json(SampleTossPaymentConst.cancelRequestJson)
|
||||
}
|
||||
|
||||
test("성공 응답") {
|
||||
commonAction().andRespond {
|
||||
withSuccess()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.cancelJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest
|
||||
val cancelResponse: PaymentCancel.Response = client.cancelPayment(cancelRequest)
|
||||
|
||||
assertSoftly(cancelResponse) {
|
||||
this.cancelStatus shouldBe "DONE"
|
||||
this.cancelReason shouldBe cancelRequest.cancelReason
|
||||
this.cancelAmount shouldBe cancelRequest.amount
|
||||
}
|
||||
}
|
||||
|
||||
test("500 에러 발생") {
|
||||
commonAction().andRespond {
|
||||
withStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest
|
||||
|
||||
// then
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
client.cancelPayment(cancelRequest)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.PAYMENT_SERVER_ERROR
|
||||
this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]"
|
||||
this.httpStatus shouldBe HttpStatus.INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user