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));
}
}
}
相关文章
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).
阅读文章 →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).
阅读文章 →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).
阅读文章 →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).
阅读文章 →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).
阅读文章 →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).
阅读文章 →