From 68857e1f0972a33914304afd466a1ee4e9208066 Mon Sep 17 00:00:00 2001 From: HyuckJuneHong Date: Wed, 27 Sep 2023 13:38:33 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[JT-80]=20test=20:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shop/jtoon/factory/PaymentFactory.java | 10 +++ .../jtoon/factory/PaymentSnippetFactory.java | 8 ++ .../presentation/PaymentControllerTest.java | 74 ++++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java index bb5d7fd..792b07a 100644 --- a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java +++ b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java @@ -56,4 +56,14 @@ public static CancelReq createCancelReq(PaymentReq paymentReq) { .refundHolder(paymentReq.buyerEmail()) .build(); } + + public static CancelReq createCancelReq(String impUid, String merchantUid, String name) { + return CancelReq.builder() + .impUid(impUid) + .merchantUid(merchantUid) + .reason("reason") + .checksum(CookieItem.COOKIE_ONE.getAmount()) + .refundHolder(name) + .build(); + } } diff --git a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java index b64f95b..33edbd5 100644 --- a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java +++ b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java @@ -18,4 +18,12 @@ public class PaymentSnippetFactory { fieldWithPath("buyerName").type(STRING).description("구매자 이름"), fieldWithPath("buyerPhone").type(STRING).description("구매자 전화번호") ); + + public static final RequestFieldsSnippet CANCEL_REQUEST = requestFields( + fieldWithPath("impUid").type(STRING).description("포트원 결제 고유번호"), + fieldWithPath("merchantUid").type(STRING).description("가맹점 주문번호"), + fieldWithPath("reason").type(STRING).description("환불 사유"), + fieldWithPath("checksum").type(NUMBER).description("환불 가능 금액"), + fieldWithPath("refundHolder").type(STRING).description("환불 수령자") + ); } diff --git a/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java b/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java index 9cde3d7..6b0ea0b 100644 --- a/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java +++ b/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java @@ -20,6 +20,7 @@ import shop.jtoon.factory.MemberFactory; import shop.jtoon.factory.PaymentFactory; import shop.jtoon.factory.PaymentSnippetFactory; +import shop.jtoon.payment.request.CancelReq; import shop.jtoon.payment.request.PaymentReq; import shop.jtoon.repository.MemberRepository; import shop.jtoon.repository.PaymentInfoRepository; @@ -94,7 +95,7 @@ void validatePayment_Amount() throws Exception { .andExpect(content().string(paymentReq.amount().toString())); } - @DisplayName("POST: /payments/validation - 결제 승인 정보가 조회되지 않거나 실제 결제 금액과 요청 금액이 다를 때, - IamportException") + @DisplayName("POST: /payments/validation - 아임포트 서버에서 결제 정보가 조회되지 않거나, 조회된 결제 금액과 환불될 금액이 다를 때, - IamportException") @WithCurrentUser @Test void validatePayment_IamportException() throws Exception { @@ -116,7 +117,7 @@ void validatePayment_IamportException() throws Exception { .andExpect(status().isInternalServerError()); } - @DisplayName("POST: /payments/validation - 결제 정보의 쿠키 가격과 실제 서버에서 알고 있는 쿠키 가격이 다를 때, - InvalidRequestException") + @DisplayName("POST: /payments/validation - 결제된 금액과 서버에서 알고 있는 금액이 다를 때, - InvalidRequestException") @WithCurrentUser @Test void validatePayment_InvalidRequestException() throws Exception { @@ -188,4 +189,73 @@ void validatePayment_MerchantUid_DuplicatedException() throws Exception { .andExpect(status().isConflict()) .andExpect(jsonPath("$.message").value(PAYMENT_MERCHANT_UID_DUPLICATED.getMessage())); } + + @DisplayName("POST: /payments/cancel - 결제 취소 요청 후 결제 취소 정보에 대해 검증 및 취소 요청이 성공적으로 됐을 때, - Void") + @Test + void cancelPayment_Void() throws Exception { + // Given + CancelReq cancelReq = PaymentFactory.createCancelReq(impUid, merchantUid, member.getName()); + willDoNothing() + .given(iamportService) + .validateIamport(any(String.class), any(BigDecimal.class)); + willDoNothing() + .given(iamportService) + .cancelIamport(any(String.class), any(String.class), any(BigDecimal.class), any(String.class)); + + // When, Then + mockMvc.perform(post("/payments/cancel") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(cancelReq))) + .andDo(print()) + .andDo(document("payments/cancel", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + PaymentSnippetFactory.CANCEL_REQUEST)) + .andExpect(status().isOk()); + } + + @DisplayName("POST: /payments/cancel - 아임포트 서버에서 결제 취소 요청을 실패할 때, - IamportException") + @Test + void cancelPayment_IamportException() throws Exception { + // Given + CancelReq cancelReq = PaymentFactory.createCancelReq(impUid, merchantUid, member.getName()); + willThrow(IamportException.class) + .given(iamportService) + .validateIamport(any(String.class), any(BigDecimal.class)); + + // When, Then + mockMvc.perform(post("/payments/cancel") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(cancelReq))) + .andDo(print()) + .andDo(document("payments/cancel", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + PaymentSnippetFactory.CANCEL_REQUEST)) + .andExpect(status().isInternalServerError()); + } + + @DisplayName("POST: /payments/cancel - 아임포트 서버에서 조회된 결제 금액과 환불될 금액이 다를 때, - IamportException") + @Test + void cancelPayment_validate_IamportException() throws Exception { + // Given + CancelReq cancelReq = PaymentFactory.createCancelReq(impUid, merchantUid, member.getName()); + willDoNothing() + .given(iamportService) + .validateIamport(any(String.class), any(BigDecimal.class)); + willThrow(IamportException.class) + .given(iamportService) + .cancelIamport(any(String.class), any(String.class), any(BigDecimal.class), any(String.class)); + + // When, Then + mockMvc.perform(post("/payments/cancel") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(cancelReq))) + .andDo(print()) + .andDo(document("payments/cancel", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + PaymentSnippetFactory.CANCEL_REQUEST)) + .andExpect(status().isInternalServerError()); + } } From 4b627f4f832c07178f4f6214def0bdbcc35646ad Mon Sep 17 00:00:00 2001 From: HyuckJuneHong Date: Wed, 27 Sep 2023 16:00:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[JT-80]=20test:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app-api/src/docs/asciidoc/payment.adoc | 30 ++++++ .../src/main/resources/static/docs/index.html | 2 +- .../main/resources/static/docs/payment.html | 89 ++++++++++++++++- .../shop/jtoon/factory/PaymentFactory.java | 11 +++ .../jtoon/factory/PaymentSnippetFactory.java | 18 +++- .../presentation/PaymentControllerTest.java | 95 ++++++++++++++++++- 6 files changed, 235 insertions(+), 10 deletions(-) diff --git a/module-application/app-api/src/docs/asciidoc/payment.adoc b/module-application/app-api/src/docs/asciidoc/payment.adoc index 89b68a6..40ce787 100644 --- a/module-application/app-api/src/docs/asciidoc/payment.adoc +++ b/module-application/app-api/src/docs/asciidoc/payment.adoc @@ -1,4 +1,5 @@ == 결제(Payment) + 결제에 대한 검증/등록/조회/취소 기능을 제공합니다. === 결제 검증 및 생성 @@ -17,3 +18,32 @@ include::{snippets}/payments/validation/http-request.adoc[] ==== 응답 include::{snippets}/payments/validation/http-response.adoc[] + +=== 결제 취소 + + 1) 결제 금액(IamportClient에서 제공)과 취소될 금액(checksum)를 비교해 검증합니다. + 2) 검증 후 결제 취소합니다. + +[discrete] +==== 요청 + +include::{snippets}/payments/cancel/http-request.adoc[] + +[discrete] +==== 응답 + +include::{snippets}/payments/cancel/http-response.adoc[] + +=== 결제 내역 조회 + + 1) 데이터베이스에 저장된 결제 내역을 조회합니다. + +[discrete] +==== 요청 + +include::{snippets}/payments/search/http-request.adoc[] + +[discrete] +==== 응답 + +include::{snippets}/payments/search/http-response.adoc[] diff --git a/module-application/app-api/src/main/resources/static/docs/index.html b/module-application/app-api/src/main/resources/static/docs/index.html index 52d80e8..5f082a3 100644 --- a/module-application/app-api/src/main/resources/static/docs/index.html +++ b/module-application/app-api/src/main/resources/static/docs/index.html @@ -619,7 +619,7 @@

diff --git a/module-application/app-api/src/main/resources/static/docs/payment.html b/module-application/app-api/src/main/resources/static/docs/payment.html index e443e20..e194da4 100644 --- a/module-application/app-api/src/main/resources/static/docs/payment.html +++ b/module-application/app-api/src/main/resources/static/docs/payment.html @@ -500,13 +500,100 @@

응답

+
+

결제 취소

+
+
+
1) 결제 금액(IamportClient에서 제공)과 취소될 금액(checksum)를 비교해 검증합니다.
+2) 검증 후 결제 취소합니다.
+
+
+

요청

+
+
+
POST /payments/cancel HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 137
+Host: localhost:8080
+
+{
+  "impUid" : "impUid123",
+  "merchantUid" : "merchant123",
+  "reason" : "reason",
+  "checksum" : 1000,
+  "refundHolder" : "홍도산"
+}
+
+
+

응답

+
+
+
HTTP/1.1 500 Internal Server Error
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 0
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Length: 22
+
+{
+  "message" : null
+}
+
+
+
+
+

결제 내역 조회

+
+
+
1) 데이터베이스에 저장된 결제 내역을 조회합니다.
+
+
+

요청

+
+
+
POST /payments/search HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Content-Length: 43
+Host: localhost:8080
+
+{
+  "merchantsUid" : [ "empty merchant" ]
+}
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 0
+Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+Pragma: no-cache
+Expires: 0
+X-Frame-Options: DENY
+Content-Length: 3
+
+[ ]
+
+
+
diff --git a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java index 792b07a..32d20b6 100644 --- a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java +++ b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentFactory.java @@ -4,9 +4,12 @@ import shop.jtoon.entity.Member; import shop.jtoon.entity.PaymentInfo; import shop.jtoon.payment.request.CancelReq; +import shop.jtoon.payment.request.ConditionReq; import shop.jtoon.payment.request.PaymentReq; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; public class PaymentFactory { @@ -66,4 +69,12 @@ public static CancelReq createCancelReq(String impUid, String merchantUid, Strin .refundHolder(name) .build(); } + + public static ConditionReq createConditionReq(String... merchantUid) { + List merchantsUid = new ArrayList<>(List.of(merchantUid)); + + return ConditionReq.builder() + .merchantsUid(merchantsUid) + .build(); + } } diff --git a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java index 33edbd5..a0e4640 100644 --- a/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java +++ b/module-application/app-api/src/test/java/shop/jtoon/factory/PaymentSnippetFactory.java @@ -1,11 +1,10 @@ package shop.jtoon.factory; import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; -import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; -import static org.springframework.restdocs.payload.JsonFieldType.STRING; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; public class PaymentSnippetFactory { public static final RequestFieldsSnippet PAYMENT_REQUEST = requestFields( @@ -26,4 +25,15 @@ public class PaymentSnippetFactory { fieldWithPath("checksum").type(NUMBER).description("환불 가능 금액"), fieldWithPath("refundHolder").type(STRING).description("환불 수령자") ); + + public static final RequestFieldsSnippet CONDITION_REQUEST = requestFields( + fieldWithPath("merchantsUid").type(ARRAY).description("가맹점 주문번호 리스트") + ); + + public static final ResponseFieldsSnippet CONDITION_RESPONSE = responseFields( + fieldWithPath("[].itemName").type(STRING).description("상품명"), + fieldWithPath("[].itemCount").type(NUMBER).description("상품 수량"), + fieldWithPath("[].amount").type(NUMBER).description("결제 금액"), + fieldWithPath("[].createdAt").type(STRING).description("결제 일시") + ); } diff --git a/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java b/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java index 6b0ea0b..5a7c2f0 100644 --- a/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java +++ b/module-application/app-api/src/test/java/shop/jtoon/payment/presentation/PaymentControllerTest.java @@ -21,13 +21,16 @@ import shop.jtoon.factory.PaymentFactory; import shop.jtoon.factory.PaymentSnippetFactory; import shop.jtoon.payment.request.CancelReq; +import shop.jtoon.payment.request.ConditionReq; import shop.jtoon.payment.request.PaymentReq; import shop.jtoon.repository.MemberRepository; import shop.jtoon.repository.PaymentInfoRepository; import shop.jtoon.service.IamportService; import java.math.BigDecimal; +import java.util.List; +import static org.hamcrest.Matchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.BDDMockito.willThrow; @@ -92,6 +95,7 @@ void validatePayment_Amount() throws Exception { preprocessResponse(prettyPrint()), PaymentSnippetFactory.PAYMENT_REQUEST)) .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(paymentReq.amount().toString())); } @@ -114,7 +118,8 @@ void validatePayment_IamportException() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), PaymentSnippetFactory.PAYMENT_REQUEST)) - .andExpect(status().isInternalServerError()); + .andExpect(status().isInternalServerError()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } @DisplayName("POST: /payments/validation - 결제된 금액과 서버에서 알고 있는 금액이 다를 때, - InvalidRequestException") @@ -137,6 +142,7 @@ void validatePayment_InvalidRequestException() throws Exception { preprocessResponse(prettyPrint()), PaymentSnippetFactory.PAYMENT_REQUEST)) .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(PAYMENT_AMOUNT_INVALID.getMessage())); } @@ -162,6 +168,7 @@ void validatePayment_ImpUid_DuplicatedException() throws Exception { preprocessResponse(prettyPrint()), PaymentSnippetFactory.PAYMENT_REQUEST)) .andExpect(status().isConflict()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.message").value(PAYMENT_IMP_UID_DUPLICATED.getMessage())); } @@ -187,7 +194,8 @@ void validatePayment_MerchantUid_DuplicatedException() throws Exception { preprocessResponse(prettyPrint()), PaymentSnippetFactory.PAYMENT_REQUEST)) .andExpect(status().isConflict()) - .andExpect(jsonPath("$.message").value(PAYMENT_MERCHANT_UID_DUPLICATED.getMessage())); + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.message", is(PAYMENT_MERCHANT_UID_DUPLICATED.getMessage()))); } @DisplayName("POST: /payments/cancel - 결제 취소 요청 후 결제 취소 정보에 대해 검증 및 취소 요청이 성공적으로 됐을 때, - Void") @@ -232,7 +240,8 @@ void cancelPayment_IamportException() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), PaymentSnippetFactory.CANCEL_REQUEST)) - .andExpect(status().isInternalServerError()); + .andExpect(status().isInternalServerError()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } @DisplayName("POST: /payments/cancel - 아임포트 서버에서 조회된 결제 금액과 환불될 금액이 다를 때, - IamportException") @@ -256,6 +265,84 @@ void cancelPayment_validate_IamportException() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), PaymentSnippetFactory.CANCEL_REQUEST)) - .andExpect(status().isInternalServerError()); + .andExpect(status().isInternalServerError()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + } + + @DisplayName("POST: /payments/search - 한 사용자에 대해 모든 결제 내역을 조회 했을 때, - List") + @WithCurrentUser + @Test + void getPayments_PaymentInfo_List() throws Exception { + // Given + ConditionReq conditionReq = PaymentFactory.createConditionReq(); + PaymentInfo paymentInfo1 = PaymentFactory.createPaymentInfo(impUid + "1", merchantUid + "1", member); + PaymentInfo paymentInfo2 = PaymentFactory.createPaymentInfo(impUid + "2", merchantUid + "2", member); + PaymentInfo paymentInfo3 = PaymentFactory.createPaymentInfo(impUid + "3", merchantUid + "3", member); + paymentInfoRepository.saveAll(List.of(paymentInfo1, paymentInfo2, paymentInfo3)); + + // When, Then + mockMvc.perform(post("/payments/search") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(conditionReq))) + .andDo(print()) + .andDo(document("payments/search", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + PaymentSnippetFactory.CONDITION_REQUEST, + PaymentSnippetFactory.CONDITION_RESPONSE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(3))); + } + + @DisplayName("POST: /payments/search - 한 사용자에 대해 특정 결제 내역을 조회 했을 때, - List") + @WithCurrentUser + @Test + void getPayments_PaymentInfo() throws Exception { + // Given + ConditionReq conditionReq = PaymentFactory.createConditionReq(merchantUid + "1"); + PaymentInfo paymentInfo1 = PaymentFactory.createPaymentInfo(impUid + "1", merchantUid + "1", member); + PaymentInfo paymentInfo2 = PaymentFactory.createPaymentInfo(impUid + "2", merchantUid + "2", member); + paymentInfoRepository.saveAll(List.of(paymentInfo1, paymentInfo2)); + + // When, Then + mockMvc.perform(post("/payments/search") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(conditionReq))) + .andDo(print()) + .andDo(document("payments/search", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + PaymentSnippetFactory.CONDITION_REQUEST, + PaymentSnippetFactory.CONDITION_RESPONSE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].itemName", is(paymentInfo1.getCookieItem().getItemName()))) + .andExpect(jsonPath("$[0].itemCount", is((paymentInfo1.getCookieItem().getCount())))) + .andExpect(jsonPath("$[0].amount", is(paymentInfo1.getAmount().intValue()))); + } + + @DisplayName("POST: /payments/search - 한 사용자에 대해 존재하지 않는 결제 내역을 조회 했을 때, - Empty List") + @WithCurrentUser + @Test + void getPayments_Empty_List() throws Exception { + // Given + ConditionReq conditionReq = PaymentFactory.createConditionReq("empty merchant"); + PaymentInfo paymentInfo = PaymentFactory.createPaymentInfo(impUid, merchantUid, member); + paymentInfoRepository.save(paymentInfo); + + // When, Then + mockMvc.perform(post("/payments/search") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(conditionReq))) + .andDo(print()) + .andDo(document("payments/search", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + PaymentSnippetFactory.CONDITION_REQUEST)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", empty())); } }