diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 71cbae1..f18c49a 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -2,6 +2,15 @@ networks: atdd-net: external: true +x-db-env: &db_env + SPRING_DATASOURCE_URL: "jdbc:mysql://atdd-db:3306/atdd?useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul" + SPRING_DATASOURCE_USERNAME: "root" + SPRING_DATASOURCE_PASSWORD: "secret" + +x-wait-db: &wait_db_cmd > + sh -c "until nc -z atdd-db 3306; do echo 'waiting db...'; sleep 2; done; + java $JAVA_OPTS -jar /app/app.jar" + services: kiosk: build: @@ -20,13 +29,10 @@ services: KIOSK_ADMIN_AUTH_PASSWORD: "admin123" KIOSK_ADMIN_AUTH_COOKIE_NAME: "AUTH_TOKEN" RESERVATION_BASE_URL: "http://reservation:8080" - SPRING_DATASOURCE_URL: "jdbc:mysql://atdd-db:3306/atdd?useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul" - SPRING_DATASOURCE_USERNAME: "root" - SPRING_DATASOURCE_PASSWORD: "secret" - - command: > - sh -c "until nc -z atdd-db 3306; do echo 'waiting db...'; sleep 2; done; - java $JAVA_OPTS -jar /app/app.jar" + KIOSK_PAYMENT_BASE_URL: "http://payments-mock:8080" + KIOSK_PAYMENT_SECRET_KEY: "test_sk_dummy" + <<: *db_env + command: *wait_db_cmd admin: build: @@ -41,12 +47,8 @@ services: JAVA_OPTS: "-Xms256m -Xmx512m" RESERVATION_BASE_URL: "http://reservation:8080" KIOSK_BASE_URL: "http://kiosk:8080" - SPRING_DATASOURCE_URL: "jdbc:mysql://atdd-db:3306/atdd?useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul" - SPRING_DATASOURCE_USERNAME: "root" - SPRING_DATASOURCE_PASSWORD: "secret" - command: > - sh -c "until nc -z atdd-db 3306; do echo 'waiting db...'; sleep 2; done; - java $JAVA_OPTS -jar /app/app.jar" + <<: *db_env + command: *wait_db_cmd reservation: build: @@ -61,9 +63,18 @@ services: JAVA_OPTS: "-Xms256m -Xmx512m" ADMIN_BASE_URL: "http://admin:8080" KIOSK_BASE_URL: "http://kiosk:8080" - SPRING_DATASOURCE_URL: "jdbc:mysql://atdd-db:3306/atdd?useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul" - SPRING_DATASOURCE_USERNAME: "root" - SPRING_DATASOURCE_PASSWORD: "secret" - command: > - sh -c "until nc -z atdd-db 3306; do echo 'waiting db...'; sleep 2; done; - java $JAVA_OPTS -jar /app/app.jar" + <<: *db_env + command: *wait_db_cmd + + payments-mock: + image: wiremock/wiremock:3.13.1 + container_name: payments-mock + networks: [ atdd-net ] + ports: + - "9090:8080" + volumes: + - ./wiremock:/home/wiremock + command: + - "--global-response-templating" + - "--verbose" + diff --git a/infra/wiremock/mappings/payment-20-confirm-success.json b/infra/wiremock/mappings/payment-20-confirm-success.json new file mode 100644 index 0000000..2bb32a5 --- /dev/null +++ b/infra/wiremock/mappings/payment-20-confirm-success.json @@ -0,0 +1,30 @@ +{ + "priority": 20, + "request": { + "method": "POST", + "urlPath": "/v1/payments/confirm", + "headers": { + "Authorization": { "equalTo": "Basic dGVzdF9za19kdW1teTo=" }, + "Content-Type": { "contains": "application/json" } + }, + "bodyPatterns": [ + { "matchesJsonPath": "$.paymentKey" }, + { "matchesJsonPath": "$.orderId" }, + { "matchesJsonPath": "$.amount" } + ] + }, + "response": { + "status": 200, + "headers": { "Content-Type": "application/json" }, + "jsonBody": { + "paymentKey": "{{jsonPath request.body '$.paymentKey'}}", + "orderId": "{{jsonPath request.body '$.orderId'}}", + "method": "CARD", + "approvedAt": "2024-01-01T12:34:56Z", + "totalAmount": "{{jsonPath request.body '$.amount'}}", + "status": "APPROVED", + "receipt": { "url": "https://pay.local/receipts/{{jsonPath request.body '$.paymentKey'}}" } + }, + "transformers": ["response-template"] + } +} diff --git a/infra/wiremock/mappings/payment-21-confirm-amount-mismatch.json b/infra/wiremock/mappings/payment-21-confirm-amount-mismatch.json new file mode 100644 index 0000000..3e06dd8 --- /dev/null +++ b/infra/wiremock/mappings/payment-21-confirm-amount-mismatch.json @@ -0,0 +1,14 @@ +{ + "priority": 19, + "request": { + "method": "POST", + "urlPath": "/v1/payments/confirm", + "headers": { "Authorization": { "equalTo": "Basic dGVzdF9za19kdW1teTo=" } }, + "bodyPatterns": [ { "matchesJsonPath": "$[?(@.amount == 99999)]" } ] + }, + "response": { + "status": 409, + "headers": { "Content-Type": "application/json" }, + "jsonBody": { "code": "AMOUNT_MISMATCH", "message": "amount/orderId mismatch" } + } +} diff --git a/src/test/java/com/camping/tests/steps/KioskPaymentStepDef.java b/src/test/java/com/camping/tests/steps/KioskPaymentStepDef.java new file mode 100644 index 0000000..5da296e --- /dev/null +++ b/src/test/java/com/camping/tests/steps/KioskPaymentStepDef.java @@ -0,0 +1,61 @@ +package com.camping.tests.steps; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import io.cucumber.java.PendingException; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import io.restassured.response.Response; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpStatus; + +public class KioskPaymentStepDef { + + private int amount; + private Response response; + + @Given("결제 금액이 {int}원인 장바구니가 있다") + public void 결제금액이원인장바구니가있다(int amount) { + this.amount = amount; + } + + @When("결제키 {string} 와 주문번호 {string} 로 결제 승인을 요청한다") + public void 결제키와주문번호로결제승인을요청한다(String paymentKey, String orderId) { + Map body = Map.of( + "paymentKey", paymentKey, + "orderId", orderId, + "amount", amount, + "items", List.of(Map.of( + "productId", 1, + "quantity", 1, + "lineTotal", amount + )) + ); + + response = given() + .contentType("application/json") + .body(body) + .when().post("http://localhost:8080/api/payments/confirm") + .then().extract().response(); + } + + + @Then("결제가 성공하며 결제 금액은 {int}원이다") + public void 결제가성공하며결제금액은원이다(int expectedAmount) { + assertThat(response.statusCode()).isEqualTo(HttpStatus.SC_OK); + boolean success = response.jsonPath().getBoolean("success"); + int paid = response.jsonPath().getInt("paidAmount"); + assertThat(success).isTrue(); + assertThat(paid).isEqualTo(expectedAmount); + } + + @Then("결제가 실패한다.") + public void 결제가실패한다() { + assertThat(response.statusCode()).isEqualTo(HttpStatus.SC_OK); + boolean success = response.jsonPath().getBoolean("success"); + assertThat(success).isFalse(); + } +} diff --git a/src/test/resources/features/kiosk-payment.feature b/src/test/resources/features/kiosk-payment.feature new file mode 100644 index 0000000..538b5ff --- /dev/null +++ b/src/test/resources/features/kiosk-payment.feature @@ -0,0 +1,11 @@ +Feature: 키오스크 결제 승인 E2E 테스트 + + Scenario: 결제 승인을 요청하면 결제가 정상 승인된다. + Given 결제 금액이 10000원인 장바구니가 있다 + When 결제키 "pay_123" 와 주문번호 "ord_123" 로 결제 승인을 요청한다 + Then 결제가 성공하며 결제 금액은 10000원이다 + + Scenario: 결제 승인을 요청하면 결제가 실패한다. + Given 결제 금액이 99999원인 장바구니가 있다 + When 결제키 "pay_fail" 와 주문번호 "ord_fail" 로 결제 승인을 요청한다 + Then 결제가 실패한다. \ No newline at end of file diff --git a/src/test/resources/features/kiosk-product.feature b/src/test/resources/features/kiosk-product.feature index 253439b..7893ad3 100644 --- a/src/test/resources/features/kiosk-product.feature +++ b/src/test/resources/features/kiosk-product.feature @@ -1,4 +1,4 @@ -Feature: 키오스크 앱 상품 조회 +Feature: 키오스크 앱 상품 조회 E2E 테스트 Scenario: 상품 조회를 요청하면 상품 목록이 잘 조회된다. When 키오스크 앱으로 상품 조회 요청을 보낸다