S SmartDocs
시리즈: Bridget java 528 줄 · 업데이트 2026-04-18

PaymentGatewayServiceTest.java

Bridget/payment_solutions/src/test/java/com/checkout/payment/gateway/service/PaymentGatewayServiceTest.java

package com.checkout.payment.gateway.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.checkout.payment.gateway.client.BankClient;
import com.checkout.payment.gateway.entity.IdempotencyEntity;
import com.checkout.payment.gateway.entity.PaymentEntity;
import com.checkout.payment.gateway.enums.PaymentStatus;
import com.checkout.payment.gateway.exception.BankServiceException;
import com.checkout.payment.gateway.exception.EventProcessingException;
import com.checkout.payment.gateway.exception.IdempotencyConflictException;
import com.checkout.payment.gateway.model.BankRequest;
import com.checkout.payment.gateway.model.BankResponse;
import com.checkout.payment.gateway.model.GetPaymentResponse;
import com.checkout.payment.gateway.model.PostPaymentRequest;
import com.checkout.payment.gateway.model.PostPaymentResponse;
import com.checkout.payment.gateway.model.ProcessPaymentResult;
import com.checkout.payment.gateway.repository.IdempotencyJpaRepository;
import com.checkout.payment.gateway.repository.PaymentJpaRepository;
import com.checkout.payment.gateway.security.RequestFingerprinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class PaymentGatewayServiceTest {

    @Mock
    private PaymentJpaRepository paymentJpaRepository;

    @Mock
    private IdempotencyJpaRepository idempotencyJpaRepository;

    @Mock
    private BankClient bankClient;

    private ObjectMapper objectMapper;
    private RequestFingerprinter requestFingerprinter;
    private PaymentGatewayService service;

    private PostPaymentRequest validRequest;

    @BeforeEach
    void setUp() {
        objectMapper = new ObjectMapper();
        requestFingerprinter = new RequestFingerprinter(objectMapper);
        service = new PaymentGatewayService(
                paymentJpaRepository,
                idempotencyJpaRepository,
                bankClient,
                objectMapper,
                requestFingerprinter);

        validRequest = new PostPaymentRequest();
        validRequest.setCardNumber("2222405343248877");
        validRequest.setExpiryMonth(12);
        validRequest.setExpiryYear(2030);
        validRequest.setCurrency("GBP");
        validRequest.setAmount(100);
        validRequest.setCvv("123");
    }

    private PostPaymentResponse pay(String idempotencyKey, PostPaymentRequest request) {
        return service.processPayment(idempotencyKey, request).response();
    }

    @Nested
    class ProcessPayment {

        @Test
        void authorizedPayment_returnsAuthorizedStatus() {
            BankResponse bankResponse = new BankResponse();
            bankResponse.setAuthorized(true);
            bankResponse.setAuthorizationCode(UUID.randomUUID().toString());
            when(bankClient.processPayment(any(BankRequest.class))).thenReturn(bankResponse);

            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.AUTHORIZED, result.getStatus());
            assertNotNull(result.getId());
            assertEquals(8877, result.getCardNumberLastFour());
            assertEquals(12, result.getExpiryMonth());
            assertEquals(2030, result.getExpiryYear());
            assertEquals("GBP", result.getCurrency());
            assertEquals(100, result.getAmount());
            assertNull(result.getErrors());
            verify(paymentJpaRepository).save(any(PaymentEntity.class));
            verify(idempotencyJpaRepository, never()).save(any());
        }

        @Test
        void declinedPayment_returnsDeclinedStatus() {
            BankResponse bankResponse = new BankResponse();
            bankResponse.setAuthorized(false);
            bankResponse.setAuthorizationCode("");
            when(bankClient.processPayment(any(BankRequest.class))).thenReturn(bankResponse);

            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.DECLINED, result.getStatus());
            assertNotNull(result.getId());
            assertEquals(8877, result.getCardNumberLastFour());
            verify(paymentJpaRepository).save(any(PaymentEntity.class));
        }

        @Test
        void bankUnavailable_throwsBankServiceException() {
            when(bankClient.processPayment(any(BankRequest.class)))
                    .thenThrow(new BankServiceException("Bank unavailable"));

            assertThrows(BankServiceException.class,
                    () -> service.processPayment(null, validRequest));
        }

        @Test
        void getPaymentById_mapsFromEntity() {
            UUID id = UUID.randomUUID();
            PaymentEntity entity = new PaymentEntity();
            entity.setId(id);
            entity.setStatus(PaymentStatus.AUTHORIZED);
            entity.setCardNumberLastFour(8877);
            entity.setExpiryMonth(12);
            entity.setExpiryYear(2030);
            entity.setCurrency("GBP");
            entity.setAmount(100);
            entity.setCreatedAt(Instant.now());

            when(paymentJpaRepository.findById(id)).thenReturn(Optional.of(entity));

            GetPaymentResponse retrieved = service.getPaymentById(id);

            assertEquals(id, retrieved.getId());
            assertEquals(PaymentStatus.AUTHORIZED, retrieved.getStatus());
            assertEquals(8877, retrieved.getCardNumberLastFour());
            assertEquals(100, retrieved.getAmount());
            assertEquals("GBP", retrieved.getCurrency());
        }
    }

    @Nested
    class Idempotency {

        @Test
        void replay_returnsCachedResponseWithoutCallingBank() throws Exception {
            String key = "idem-1";
            PostPaymentResponse cached = new PostPaymentResponse();
            cached.setId(UUID.randomUUID());
            cached.setStatus(PaymentStatus.AUTHORIZED);
            cached.setCardNumberLastFour(8877);
            cached.setExpiryMonth(12);
            cached.setExpiryYear(2030);
            cached.setCurrency("GBP");
            cached.setAmount(100);

            IdempotencyEntity row = new IdempotencyEntity();
            row.setIdempotencyKey(key);
            row.setRequestFingerprint(requestFingerprinter.fingerprint(validRequest));
            row.setHttpStatus(200);
            row.setResponseJson(objectMapper.writeValueAsString(cached));
            row.setPaymentId(cached.getId());

            when(idempotencyJpaRepository.findById(key)).thenReturn(Optional.of(row));

            ProcessPaymentResult result = service.processPayment(key, validRequest);

            assertEquals(200, result.httpStatusCode());
            assertEquals(PaymentStatus.AUTHORIZED, result.response().getStatus());
            verify(bankClient, never()).processPayment(any());
            verify(paymentJpaRepository, never()).save(any());
        }

        @Test
        void conflictingPayload_throws() {
            String key = "idem-2";
            IdempotencyEntity row = new IdempotencyEntity();
            row.setIdempotencyKey(key);
            row.setRequestFingerprint("deadbeef");
            row.setHttpStatus(200);
            row.setResponseJson("{}");

            when(idempotencyJpaRepository.findById(key)).thenReturn(Optional.of(row));

            assertThrows(IdempotencyConflictException.class,
                    () -> service.processPayment(key, validRequest));
        }

        @Test
        void rejectedValidation_persistsIdempotencyRecord() {
            String key = "idem-3";
            validRequest.setCardNumber(null);

            ProcessPaymentResult result = service.processPayment(key, validRequest);

            assertEquals(400, result.httpStatusCode());
            assertEquals(PaymentStatus.REJECTED, result.response().getStatus());
            verify(idempotencyJpaRepository).save(any(IdempotencyEntity.class));
            verify(bankClient, never()).processPayment(any());
        }
    }

    @Nested
    class Validation {

        @Test
        void validRequest_passesValidation() {
            BankResponse bankResponse = new BankResponse();
            bankResponse.setAuthorized(true);
            when(bankClient.processPayment(any())).thenReturn(bankResponse);

            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.AUTHORIZED, result.getStatus());
        }

        @Test
        void missingCardNumber_isRejected() {
            validRequest.setCardNumber(null);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().contains("Card number is required"));
            verify(bankClient, never()).processPayment(any());
        }

        @Test
        void cardNumberTooShort_isRejected() {
            validRequest.setCardNumber("1234567890123");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("14 and 19")));
        }

        @Test
        void cardNumberTooLong_isRejected() {
            validRequest.setCardNumber("12345678901234567890");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("14 and 19")));
        }

        @Test
        void nonNumericCardNumber_isRejected() {
            validRequest.setCardNumber("2222ABCD43248877");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("numeric")));
        }

        @Test
        void missingExpiryMonth_isRejected() {
            validRequest.setExpiryMonth(null);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().contains("Expiry month is required"));
        }

        @Test
        void expiryMonthOutOfRange_isRejected() {
            validRequest.setExpiryMonth(13);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("between 1 and 12")));
        }

        @Test
        void expiryMonthZero_isRejected() {
            validRequest.setExpiryMonth(0);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("between 1 and 12")));
        }

        @Test
        void missingExpiryYear_isRejected() {
            validRequest.setExpiryYear(null);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().contains("Expiry year is required"));
        }

        @Test
        void expiredCard_isRejected() {
            validRequest.setExpiryMonth(1);
            validRequest.setExpiryYear(2020);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("future")));
        }

        @Test
        void missingCurrency_isRejected() {
            validRequest.setCurrency(null);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().contains("Currency is required"));
        }

        @Test
        void invalidCurrencyLength_isRejected() {
            validRequest.setCurrency("US");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("ISO 4217")));
        }

        @Test
        void invalidIsoCurrencyCode_isRejected() {
            validRequest.setCurrency("XYZ");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("not a valid ISO 4217")));
        }

        @Test
        void validIsoButUnsupportedCurrency_isRejected() {
            validRequest.setCurrency("JPY");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("USD, GBP, EUR")));
        }

        @Test
        void missingAmount_isRejected() {
            validRequest.setAmount(null);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().contains("Amount is required"));
        }

        @Test
        void negativeAmount_isRejected() {
            validRequest.setAmount(-1);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("positive")));
        }

        @Test
        void zeroAmount_isRejected() {
            validRequest.setAmount(0);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("positive")));
        }

        @Test
        void missingCvv_isRejected() {
            validRequest.setCvv(null);
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().contains("CVV is required"));
        }

        @Test
        void cvvTooShort_isRejected() {
            validRequest.setCvv("12");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("3 or 4")));
        }

        @Test
        void cvvTooLong_isRejected() {
            validRequest.setCvv("12345");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("3 or 4")));
        }

        @Test
        void nonNumericCvv_isRejected() {
            validRequest.setCvv("12A");
            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().stream()
                    .anyMatch(e -> e.contains("numeric")));
        }

        @Test
        void fourDigitCvv_isValid() {
            validRequest.setCvv("1234");
            BankResponse bankResponse = new BankResponse();
            bankResponse.setAuthorized(true);
            when(bankClient.processPayment(any())).thenReturn(bankResponse);

            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.AUTHORIZED, result.getStatus());
        }

        @Test
        void multipleErrors_areAllReported() {
            validRequest.setCardNumber(null);
            validRequest.setCurrency(null);
            validRequest.setCvv(null);
            validRequest.setAmount(null);

            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.REJECTED, result.getStatus());
            assertTrue(result.getErrors().size() >= 3);
        }

        @Test
        void supportedCurrencies_areAccepted() {
            BankResponse bankResponse = new BankResponse();
            bankResponse.setAuthorized(true);
            when(bankClient.processPayment(any())).thenReturn(bankResponse);

            for (String currency : new String[]{"USD", "GBP", "EUR"}) {
                validRequest.setCurrency(currency);
                PostPaymentResponse result = pay(null, validRequest);
                assertEquals(PaymentStatus.AUTHORIZED, result.getStatus(),
                        "Currency " + currency + " should be accepted");
            }
        }

        @Test
        void cardNumberMinLength_isAccepted() {
            validRequest.setCardNumber("22224053432481");
            BankResponse bankResponse = new BankResponse();
            bankResponse.setAuthorized(true);
            when(bankClient.processPayment(any())).thenReturn(bankResponse);

            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.AUTHORIZED, result.getStatus());
        }

        @Test
        void cardNumberMaxLength_isAccepted() {
            validRequest.setCardNumber("2222405343248877123");
            BankResponse bankResponse = new BankResponse();
            bankResponse.setAuthorized(true);
            when(bankClient.processPayment(any())).thenReturn(bankResponse);

            PostPaymentResponse result = pay(null, validRequest);

            assertEquals(PaymentStatus.AUTHORIZED, result.getStatus());
        }
    }

    @Nested
    class GetPayment {

        @Test
        void existingPayment_isReturned() {
            UUID paymentId = UUID.randomUUID();
            PaymentEntity entity = new PaymentEntity();
            entity.setId(paymentId);
            entity.setStatus(PaymentStatus.AUTHORIZED);
            entity.setCardNumberLastFour(8877);
            entity.setExpiryMonth(12);
            entity.setExpiryYear(2030);
            entity.setCurrency("GBP");
            entity.setAmount(100);
            entity.setCreatedAt(Instant.now());

            when(paymentJpaRepository.findById(paymentId)).thenReturn(Optional.of(entity));

            GetPaymentResponse result = service.getPaymentById(paymentId);

            assertEquals(paymentId, result.getId());
            assertEquals(PaymentStatus.AUTHORIZED, result.getStatus());
            assertEquals(8877, result.getCardNumberLastFour());
            assertEquals(12, result.getExpiryMonth());
            assertEquals(2030, result.getExpiryYear());
            assertEquals("GBP", result.getCurrency());
            assertEquals(100, result.getAmount());
        }

        @Test
        void nonExistentPayment_throwsException() {
            UUID paymentId = UUID.randomUUID();
            when(paymentJpaRepository.findById(paymentId)).thenReturn(Optional.empty());

            assertThrows(EventProcessingException.class,
                    () -> service.getPaymentById(paymentId));
        }
    }
}

관련 글

Bridget java 업데이트 2026-03-30

PaymentGatewayApplication.java

PaymentGatewayApplication.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/PaymentGatewayApplication.java).

글 읽기 →
Bridget java 업데이트 2026-03-20

ApplicationConfiguration.java

ApplicationConfiguration.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/configuration/ApplicationConfiguration.java).

글 읽기 →
Bridget java 업데이트 2026-03-30

RequestResponseLogger.java

RequestResponseLogger.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/configuration/RequestResponseLogger.java).

글 읽기 →
Bridget java 업데이트 2026-04-14

PaymentGatewayController.java

PaymentGatewayController.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/controller/PaymentGatewayController.java).

글 읽기 →
Bridget java 업데이트 2026-03-20

PaymentStatus.java

PaymentStatus.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/enums/PaymentStatus.java).

글 읽기 →
Bridget java 업데이트 2026-03-29

BankErrorException.java

BankErrorException.java — java source code from the Bridget learning materials (Bridget/payment-gateway-challenge-java/src/main/java/com/checkout/payment/gateway/exception/BankErrorException.java).

글 읽기 →