diff --git a/pom.xml b/pom.xml index 963df27e0..3f29c80a4 100644 --- a/pom.xml +++ b/pom.xml @@ -244,6 +244,11 @@ ${mock-web-server.version} test + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + test + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8725764ae..4b9358255 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,29 +1,89 @@ resilience4j.retry: instances: - getPaymentRequestInfo: - maxAttempts: 3 - waitDuration: 2s - enableExponentialBackoff: false newTransaction: maxAttempts: 3 waitDuration: 2s enableExponentialBackoff: false + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException getTransactionInfo: maxAttempts: 3 waitDuration: 2s enableExponentialBackoff: false + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException requestTransactionAuthorization: maxAttempts: 3 waitDuration: 2s enableExponentialBackoff: false + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException updateTransactionAuthorization: maxAttempts: 3 waitDuration: 2s enableExponentialBackoff: false - activateTransaction: + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException + cancelTransaction: + maxAttempts: 3 + waitDuration: 2s + enableExponentialBackoff: false + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException + addUserReceipt: maxAttempts: 3 waitDuration: 2s enableExponentialBackoff: false + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException resilience4j.circuitbreaker: configs: @@ -37,10 +97,38 @@ resilience4j.circuitbreaker: instances: transactions-backend: baseConfig: default + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException node-backend: baseConfig: default ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException ecommerce-db: baseConfig: default + ignoreExceptions: + - it.pagopa.transactions.exceptions.UnsatisfiablePspRequestException + - it.pagopa.transactions.exceptions.PaymentNoticeAllCCPMismatchException + - it.pagopa.transactions.exceptions.TransactionNotFoundException + - it.pagopa.transactions.exceptions.AlreadyProcessedException + - it.pagopa.transactions.exceptions.NotImplementedException + - it.pagopa.transactions.exceptions.InvalidRequestException + - it.pagopa.transactions.exceptions.TransactionAmountMismatchException + - it.pagopa.transactions.exceptions.NodoErrorException + - it.pagopa.transactions.exceptions.InvalidNodoResponseException diff --git a/src/test/java/it/pagopa/transactions/services/v1/CircuitBreakerTest.java b/src/test/java/it/pagopa/transactions/services/v1/CircuitBreakerTest.java index 350257d21..dd3523be7 100644 --- a/src/test/java/it/pagopa/transactions/services/v1/CircuitBreakerTest.java +++ b/src/test/java/it/pagopa/transactions/services/v1/CircuitBreakerTest.java @@ -1,28 +1,35 @@ package it.pagopa.transactions.services.v1; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryRegistry; import it.pagopa.ecommerce.commons.documents.PaymentNotice; import it.pagopa.ecommerce.commons.documents.PaymentTransferInformation; import it.pagopa.ecommerce.commons.documents.v1.TransactionActivatedData; import it.pagopa.ecommerce.commons.documents.v1.TransactionActivatedEvent; +import it.pagopa.ecommerce.commons.domain.PaymentToken; import it.pagopa.ecommerce.commons.domain.TransactionId; import it.pagopa.ecommerce.commons.v1.TransactionTestUtils; import it.pagopa.generated.transactions.model.CtFaultBean; -import it.pagopa.generated.transactions.server.model.ClientIdDto; -import it.pagopa.generated.transactions.server.model.NewTransactionRequestDto; -import it.pagopa.generated.transactions.server.model.PartyConfigurationFaultDto; -import it.pagopa.generated.transactions.server.model.PaymentNoticeInfoDto; +import it.pagopa.generated.transactions.server.model.*; import it.pagopa.transactions.commands.handlers.v1.TransactionActivateHandler; -import it.pagopa.transactions.exceptions.InvalidNodoResponseException; -import it.pagopa.transactions.exceptions.NodoErrorException; +import it.pagopa.transactions.exceptions.*; +import it.pagopa.transactions.repositories.TransactionsViewRepository; +import it.pagopa.transactions.utils.TransactionsUtils; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -31,8 +38,13 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.util.List; -import java.util.UUID; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static it.pagopa.ecommerce.commons.v1.TransactionTestUtils.EMAIL_STRING; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -54,9 +66,307 @@ class CircuitBreakerTest { @MockBean private TransactionActivateHandler transactionActivateHandlerV1; + @MockBean + private TransactionsViewRepository transactionsViewRepository; + @MockBean + private TransactionsUtils transactionsUtils; + @Autowired private CircuitBreakerRegistry circuitBreakerRegistry; + @Autowired + private RetryRegistry retryRegistry; + + private static final JsonNode resilience4jConfiguration; + + private static final Map exceptionMapper = Stream.of( + new UnsatisfiablePspRequestException( + new PaymentToken(""), + RequestAuthorizationRequestDto.LanguageEnum.IT, + 0 + ), + new PaymentNoticeAllCCPMismatchException("rptId", true, true), + new TransactionNotFoundException(""), + new AlreadyProcessedException(new TransactionId(TransactionTestUtils.TRANSACTION_ID)), + new NotImplementedException(""), + new InvalidRequestException(""), + new TransactionAmountMismatchException(10, 11), + new NodoErrorException(new CtFaultBean()), + new InvalidNodoResponseException("") + ).collect(Collectors.toMap(exception -> exception.getClass().getCanonicalName(), Function.identity())); + + static { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + try { + resilience4jConfiguration = mapper.readTree(new File("./src/main/resources/application.yml")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Stream getIgnoredExceptionsForRetry(String retryInstanceName) { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + resilience4jConfiguration + .get("resilience4j.retry") + .get("instances") + .get(retryInstanceName) + .get("ignoreExceptions") + .elements(), + Spliterator.ORDERED + ), + false + ) + .map(ignoredException -> { + String exceptionName = ignoredException.asText(); + return Arguments.of( + Optional + .ofNullable(exceptionMapper.get(exceptionName)) + .orElseThrow( + () -> new RuntimeException( + "Missing exception instance in test suite inside map `exceptionMapper` for class: %s" + .formatted(exceptionName) + ) + ), + retryInstanceName + ); + } + + ); + } + + private static Stream getIgnoredExceptionForNewTransactionRetry() { + return getIgnoredExceptionsForRetry("newTransaction"); + } + + private static Stream getIgnoredExceptionForGetTransactionInfoRetry() { + return getIgnoredExceptionsForRetry("getTransactionInfo"); + } + + private static Stream getIgnoredExceptionForRequestTransactionAuthorizationRetry() { + return getIgnoredExceptionsForRetry("requestTransactionAuthorization"); + } + + private static Stream getIgnoredExceptionForUpdateTransactionAuthorizationRetry() { + return getIgnoredExceptionsForRetry("updateTransactionAuthorization"); + } + + private static Stream getIgnoredExceptionForCancelTransactionRetry() { + return getIgnoredExceptionsForRetry("cancelTransaction"); + } + + private static Stream getIgnoredExceptionForAddUserReceiptRetry() { + return getIgnoredExceptionsForRetry("addUserReceipt"); + } + + @ParameterizedTest + @MethodSource("getIgnoredExceptionForNewTransactionRetry") + @Order(0) + void shouldNotPerformRetryForExcludedException_newTransactionRetry( + Exception thrownException, + String retryInstanceName + ) { + Retry retry = retryRegistry.retry(retryInstanceName); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + + 1; + ClientIdDto clientIdDto = ClientIdDto.CHECKOUT; + UUID TEST_CCP = UUID.randomUUID(); + UUID TRANSACTION_ID = UUID.randomUUID(); + + NewTransactionRequestDto transactionRequestDto = new NewTransactionRequestDto() + .email(EMAIL_STRING) + .addPaymentNoticesItem(new PaymentNoticeInfoDto().rptId(TransactionTestUtils.RPT_ID).amount(10)); + + TransactionActivatedData transactionActivatedData = new TransactionActivatedData(); + transactionActivatedData.setEmail(TransactionTestUtils.EMAIL); + transactionActivatedData + .setPaymentNotices( + List.of( + new PaymentNotice( + TransactionTestUtils.PAYMENT_TOKEN, + null, + "desc", + 0, + TEST_CCP.toString(), + List.of(new PaymentTransferInformation("77777777777", false, 0, null)), + false + ) + ) + ); + + TransactionActivatedEvent transactionActivatedEvent = new TransactionActivatedEvent( + new TransactionId(TRANSACTION_ID).value(), + transactionActivatedData + ); + + /* + * Preconditions + */ + Mockito.when(transactionActivateHandlerV1.handle(any())).thenReturn(Mono.error(thrownException)); + StepVerifier + .create( + transactionsService.newTransaction( + transactionRequestDto, + clientIdDto, + new TransactionId(transactionActivatedEvent.getTransactionId()) + ) + ) + .expectError(thrownException.getClass()) + .verify(); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + } + + @ParameterizedTest + @MethodSource("getIgnoredExceptionForGetTransactionInfoRetry") + @Order(0) + void shouldNotPerformRetryForExcludedException_getTransactionInfoRetry( + Exception thrownException, + String retryInstanceName + ) { + Retry retry = retryRegistry.retry(retryInstanceName); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + + 1; + + /* + * Preconditions + */ + Mockito.when(transactionsViewRepository.findById(any(String.class))) + .thenReturn(Mono.error(thrownException)); + + StepVerifier + .create( + transactionsService.getTransactionInfo("transactionId") + ) + .expectError(thrownException.getClass()) + .verify(); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + } + + @ParameterizedTest + @MethodSource("getIgnoredExceptionForRequestTransactionAuthorizationRetry") + @Order(0) + void shouldNotPerformRetryForExcludedException_requestTransactionAuthorizationRetry( + Exception thrownException, + String retryInstanceName + ) { + Retry retry = retryRegistry.retry(retryInstanceName); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + + 1; + + /* + * Preconditions + */ + Mockito.when(transactionsViewRepository.findById(any(String.class))) + .thenReturn(Mono.error(thrownException)); + + StepVerifier + .create( + transactionsService.requestTransactionAuthorization( + "transactionId", + "", + new RequestAuthorizationRequestDto() + ) + ) + .expectError(thrownException.getClass()) + .verify(); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + } + + @ParameterizedTest + @MethodSource("getIgnoredExceptionForUpdateTransactionAuthorizationRetry") + @Order(0) + void shouldNotPerformRetryForExcludedException_updateTransactionAuthorizationRetry( + Exception thrownException, + String retryInstanceName + ) { + Retry retry = retryRegistry.retry(retryInstanceName); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + + 1; + + /* + * Preconditions + */ + Mockito.when(transactionsUtils.reduceEvents(any(), any(), any(), any())) + .thenReturn(Mono.error(thrownException)); + + StepVerifier + .create( + transactionsService + .updateTransactionAuthorization(UUID.randomUUID(), new UpdateAuthorizationRequestDto()) + ) + .expectError(thrownException.getClass()) + .verify(); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + } + + @ParameterizedTest + @MethodSource("getIgnoredExceptionForCancelTransactionRetry") + @Order(0) + void shouldNotPerformRetryForExcludedException_cancelTransactionRetry( + Exception thrownException, + String retryInstanceName + ) { + Retry retry = retryRegistry.retry(retryInstanceName); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + + 1; + + /* + * Preconditions + */ + Mockito.when(transactionsViewRepository.findById(any(String.class))).thenReturn(Mono.error(thrownException)); + + StepVerifier + .create( + transactionsService.cancelTransaction("") + ) + .expectError(thrownException.getClass()) + .verify(); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + } + + @ParameterizedTest + @MethodSource("getIgnoredExceptionForAddUserReceiptRetry") + @Order(0) + void shouldNotPerformRetryForExcludedException_AddUserReceiptRetry( + Exception thrownException, + String retryInstanceName + ) { + Retry retry = retryRegistry.retry(retryInstanceName); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + + 1; + + /* + * Preconditions + */ + Mockito.when(transactionsViewRepository.findById(any(String.class))).thenReturn(Mono.error(thrownException)); + + StepVerifier + .create( + transactionsService.addUserReceipt("", new AddUserReceiptRequestDto()) + ) + .expectError(thrownException.getClass()) + .verify(); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + } + @Test @Order(1) void shouldNotOpenCircuitBreakerForNodoErrorException() { @@ -116,7 +426,11 @@ void shouldNotOpenCircuitBreakerForNodoErrorException() { @Test @Order(2) - void shouldOpenCircuitBreakerForNotExcludedException() { + void shouldOpenCircuitBreakerForNotExcludedExceptionPerformingRetry() { + Retry retry = retryRegistry.retry("newTransaction"); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt(); + long expectedFailedCallsWithRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithRetryAttempt() + 1; + ClientIdDto clientIdDto = ClientIdDto.CHECKOUT; UUID TEST_CCP = UUID.randomUUID(); UUID TRANSACTION_ID = UUID.randomUUID(); @@ -151,7 +465,7 @@ void shouldOpenCircuitBreakerForNotExcludedException() { * Preconditions */ Mockito.when(transactionActivateHandlerV1.handle(any())) - .thenReturn(Mono.error(new InvalidNodoResponseException("Invalid response received"))); + .thenReturn(Mono.error(new RuntimeException("Invalid response received"))); StepVerifier .create( @@ -165,6 +479,11 @@ void shouldOpenCircuitBreakerForNotExcludedException() { .verify(); CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("node-backend"); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + assertEquals(expectedFailedCallsWithRetryAttempt, retry.getMetrics().getNumberOfFailedCallsWithRetryAttempt()); } diff --git a/src/test/java/it/pagopa/transactions/services/v2/CircuitBreakerTest.java b/src/test/java/it/pagopa/transactions/services/v2/CircuitBreakerTest.java index b226bd9ca..4c7597f0e 100644 --- a/src/test/java/it/pagopa/transactions/services/v2/CircuitBreakerTest.java +++ b/src/test/java/it/pagopa/transactions/services/v2/CircuitBreakerTest.java @@ -1,29 +1,37 @@ package it.pagopa.transactions.services.v2; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryRegistry; import it.pagopa.ecommerce.commons.documents.PaymentNotice; import it.pagopa.ecommerce.commons.documents.PaymentTransferInformation; -import it.pagopa.ecommerce.commons.documents.v1.TransactionActivatedData; -import it.pagopa.ecommerce.commons.documents.v1.TransactionActivatedEvent; +import it.pagopa.ecommerce.commons.documents.v2.TransactionActivatedData; +import it.pagopa.ecommerce.commons.documents.v2.TransactionActivatedEvent; +import it.pagopa.ecommerce.commons.domain.PaymentToken; import it.pagopa.ecommerce.commons.domain.TransactionId; -import it.pagopa.ecommerce.commons.v1.TransactionTestUtils; +import it.pagopa.ecommerce.commons.v2.TransactionTestUtils; import it.pagopa.generated.transactions.model.CtFaultBean; -import it.pagopa.generated.transactions.server.model.ClientIdDto; -import it.pagopa.generated.transactions.server.model.NewTransactionRequestDto; -import it.pagopa.generated.transactions.server.model.PartyConfigurationFaultDto; -import it.pagopa.generated.transactions.server.model.PaymentNoticeInfoDto; +import it.pagopa.generated.transactions.server.model.RequestAuthorizationRequestDto; +import it.pagopa.generated.transactions.v2.server.model.ClientIdDto; +import it.pagopa.generated.transactions.v2.server.model.NewTransactionRequestDto; +import it.pagopa.generated.transactions.v2.server.model.PartyConfigurationFaultDto; +import it.pagopa.generated.transactions.v2.server.model.PaymentNoticeInfoDto; import it.pagopa.transactions.commands.handlers.v2.TransactionActivateHandler; -import it.pagopa.transactions.exceptions.InvalidNodoResponseException; -import it.pagopa.transactions.exceptions.NodoErrorException; -import it.pagopa.transactions.services.v1.TransactionsService; +import it.pagopa.transactions.exceptions.*; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -32,8 +40,13 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import java.util.List; -import java.util.UUID; +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static it.pagopa.ecommerce.commons.v1.TransactionTestUtils.EMAIL_STRING; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -53,6 +66,132 @@ class CircuitBreakerTest { @Autowired private CircuitBreakerRegistry circuitBreakerRegistry; + @Autowired + private RetryRegistry retryRegistry; + + private static final JsonNode resilience4jConfiguration; + + private static final Map exceptionMapper = Stream.of( + new UnsatisfiablePspRequestException( + new PaymentToken(""), + RequestAuthorizationRequestDto.LanguageEnum.IT, + 0 + ), + new PaymentNoticeAllCCPMismatchException("rptId", true, true), + new TransactionNotFoundException(""), + new AlreadyProcessedException( + new TransactionId(it.pagopa.ecommerce.commons.v1.TransactionTestUtils.TRANSACTION_ID) + ), + new NotImplementedException(""), + new InvalidRequestException(""), + new TransactionAmountMismatchException(10, 11), + new NodoErrorException(new CtFaultBean()), + new InvalidNodoResponseException("") + ).collect(Collectors.toMap(exception -> exception.getClass().getCanonicalName(), Function.identity())); + + static { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + try { + resilience4jConfiguration = mapper.readTree(new File("./src/main/resources/application.yml")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Stream getIgnoredExceptionsForRetry(String retryInstanceName) { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + resilience4jConfiguration + .get("resilience4j.retry") + .get("instances") + .get(retryInstanceName) + .get("ignoreExceptions") + .elements(), + Spliterator.ORDERED + ), + false + ) + .map(ignoredException -> { + String exceptionName = ignoredException.asText(); + return Arguments.of( + Optional + .ofNullable(exceptionMapper.get(exceptionName)) + .orElseThrow( + () -> new RuntimeException( + "Missing exception instance in test suite inside map `exceptionMapper` for class: %s" + ) + ), + retryInstanceName + ); + } + + ); + } + + private static Stream getIgnoredExceptionForNewTransactionRetry() { + return getIgnoredExceptionsForRetry("newTransaction"); + } + + @ParameterizedTest + @MethodSource("getIgnoredExceptionForNewTransactionRetry") + @Order(0) + void shouldNotPerformRetryForExcludedException_newTransactionRetry( + Exception thrownException, + String retryInstanceName + ) { + Retry retry = retryRegistry.retry(retryInstanceName); + long expectedFailedCallsWithoutRetryAttempt = retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + + 1; + ClientIdDto clientIdDto = ClientIdDto.CHECKOUT; + UUID TEST_CCP = UUID.randomUUID(); + UUID TRANSACTION_ID = UUID.randomUUID(); + + NewTransactionRequestDto transactionRequestDto = new NewTransactionRequestDto() + .email(EMAIL_STRING) + .addPaymentNoticesItem(new PaymentNoticeInfoDto().rptId(TransactionTestUtils.RPT_ID).amount(10)); + + TransactionActivatedData transactionActivatedData = new TransactionActivatedData(); + transactionActivatedData.setEmail(it.pagopa.ecommerce.commons.v1.TransactionTestUtils.EMAIL); + transactionActivatedData + .setPaymentNotices( + List.of( + new PaymentNotice( + it.pagopa.ecommerce.commons.v1.TransactionTestUtils.PAYMENT_TOKEN, + null, + "desc", + 0, + TEST_CCP.toString(), + List.of(new PaymentTransferInformation("77777777777", false, 0, null)), + false + ) + ) + ); + + TransactionActivatedEvent transactionActivatedEvent = new TransactionActivatedEvent( + new TransactionId(TRANSACTION_ID).value(), + transactionActivatedData + ); + + /* + * Preconditions + */ + Mockito.when(transactionActivateHandlerV2.handle(any())).thenReturn(Mono.error(thrownException)); + StepVerifier + .create( + transactionsService.newTransaction( + transactionRequestDto, + clientIdDto, + new TransactionId(transactionActivatedEvent.getTransactionId()) + ) + ) + .expectError(thrownException.getClass()) + .verify(); + assertEquals( + expectedFailedCallsWithoutRetryAttempt, + retry.getMetrics().getNumberOfFailedCallsWithoutRetryAttempt() + ); + } + @Test @Order(1) void shouldNotOpenCircuitBreakerForNodoErrorException() { @@ -147,7 +286,7 @@ void shouldOpenCircuitBreakerForNotExcludedException() { * Preconditions */ Mockito.when(transactionActivateHandlerV2.handle(any())) - .thenReturn(Mono.error(new InvalidNodoResponseException("Invalid response received"))); + .thenReturn(Mono.error(new RuntimeException("Invalid response received"))); StepVerifier .create(