diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000..4bbacea2 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,3 @@ +name: "CodeQL config" +paths-ignore: + - "**/src/test/**" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index df054d6e..abf2415b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,6 +35,7 @@ jobs: with: languages: ${{ matrix.language }} queries: security-extended + config-file: ./.github/codeql/codeql-config.yml - uses: actions/setup-java@v5 with: diff --git a/apiTest/docker-compose.yml b/apiTest/docker-compose.yml index e3c5eaeb..dffd82a7 100644 --- a/apiTest/docker-compose.yml +++ b/apiTest/docker-compose.yml @@ -32,7 +32,7 @@ services: - "9999:9999" volumes: - ./src/test/resources/mappings:/home/wiremock/mappings - - ./src/test/resources/__files:/home/wiremock/__files + - ./src/test/resources/files:/home/wiremock/files command: - "--global-response-templating" - "--verbose" diff --git a/src/main/java/uk/gov/hmcts/cp/subscription/config/AppProperties.java b/src/main/java/uk/gov/hmcts/cp/subscription/config/AppProperties.java index 86cb5736..0ed14bd9 100644 --- a/src/main/java/uk/gov/hmcts/cp/subscription/config/AppProperties.java +++ b/src/main/java/uk/gov/hmcts/cp/subscription/config/AppProperties.java @@ -10,12 +10,18 @@ @Service public class AppProperties { - private int materialRetryIntervalMilliSecs; - private int materialRetryTimeoutMilliSecs; + private final int materialRetryIntervalMilliSecs; + private final int materialRetryTimeoutMilliSecs; + private final int callbackRetryIntervalMilliSecs; + private final int callbackRetryTimeoutMilliSecs; public AppProperties(@Value("${material-client.retry.intervalMilliSecs}") final int materialRetryIntervalMilliSecs, - @Value("${material-client.retry.timeoutMilliSecs}") final int materialRetryTimeoutMilliSecs) { + @Value("${material-client.retry.timeoutMilliSecs}") final int materialRetryTimeoutMilliSecs, + @Value("${callback-client.retry.intervalMilliSecs}") final int callbackRetryIntervalMilliSecs, + @Value("${callback-client.retry.timeoutMilliSecs}") final int callbackRetryTimeoutMilliSecs) { this.materialRetryIntervalMilliSecs = materialRetryIntervalMilliSecs; this.materialRetryTimeoutMilliSecs = materialRetryTimeoutMilliSecs; + this.callbackRetryIntervalMilliSecs = callbackRetryIntervalMilliSecs; + this.callbackRetryTimeoutMilliSecs = callbackRetryTimeoutMilliSecs; } } diff --git a/src/main/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandler.java b/src/main/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandler.java index 6de5c704..b5f86837 100644 --- a/src/main/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandler.java +++ b/src/main/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandler.java @@ -20,6 +20,9 @@ @Slf4j public class GlobalExceptionHandler { + private static final String CALLBACK_NOT_READY = "Callback is not ready"; + private static final String MATERIAL_NOT_READY = "Material metadata not ready"; + @ExceptionHandler({EntityNotFoundException.class, NoHandlerFoundException.class}) public ResponseEntity handleNotFoundException(final Exception exception) { log.error("NotFoundException {}", exception.getMessage()); @@ -83,9 +86,14 @@ public ResponseEntity handleUnsupportedOperation(final UnsupportedOperat @ExceptionHandler(ConditionTimeoutException.class) public ResponseEntity handleConditionTimeout(final ConditionTimeoutException ex) { - log.error("Material metadata timed out: {}", ex.getMessage()); + final boolean isCallbackTimeout = CALLBACK_NOT_READY.equals(ex.getMessage()); + if (isCallbackTimeout) { + log.error("Callback delivery timed out: {}", ex.getMessage()); + } else { + log.error("Material metadata timed out: {}", ex.getMessage()); + } return ResponseEntity .status(HttpStatus.GATEWAY_TIMEOUT) - .body("Material metadata not ready"); + .body(isCallbackTimeout ? CALLBACK_NOT_READY : MATERIAL_NOT_READY); } } \ No newline at end of file diff --git a/src/main/java/uk/gov/hmcts/cp/subscription/services/CallbackService.java b/src/main/java/uk/gov/hmcts/cp/subscription/services/CallbackService.java index 5201c17e..93a6ba27 100644 --- a/src/main/java/uk/gov/hmcts/cp/subscription/services/CallbackService.java +++ b/src/main/java/uk/gov/hmcts/cp/subscription/services/CallbackService.java @@ -2,18 +2,46 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.awaitility.core.ConditionTimeoutException; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; + import uk.gov.hmcts.cp.openapi.model.EventNotificationPayload; import uk.gov.hmcts.cp.subscription.clients.CallbackClient; +import uk.gov.hmcts.cp.subscription.config.AppProperties; + +import java.time.Duration; + +import static org.awaitility.Awaitility.await; @Service @Slf4j @RequiredArgsConstructor public class CallbackService { + private final AppProperties appProperties; private final CallbackClient callbackClient; public void sendToSubscriber(final String url, final EventNotificationPayload eventNotificationPayload) { - callbackClient.sendNotification(url, eventNotificationPayload); + try { + waitForCallbackDelivery(url, eventNotificationPayload); + } catch (ConditionTimeoutException e) { + throw new ConditionTimeoutException("Callback is not ready", e); + } + } + + private void waitForCallbackDelivery(final String url, final EventNotificationPayload eventNotificationPayload) { + await() + .pollInterval(Duration.ofMillis(appProperties.getCallbackRetryIntervalMilliSecs())) + .atMost(Duration.ofMillis(appProperties.getCallbackRetryTimeoutMilliSecs())) + .until(() -> { + try { + callbackClient.sendNotification(url, eventNotificationPayload); + return true; + } catch (RestClientException e) { + log.warn("Callback delivery failed for {}, retrying: {}", url, e.getMessage()); + return false; + } + }); } } diff --git a/src/main/java/uk/gov/hmcts/cp/subscription/services/exceptions/CallbackUrlDeliveryException.java b/src/main/java/uk/gov/hmcts/cp/subscription/services/exceptions/CallbackUrlDeliveryException.java deleted file mode 100644 index 4ff77a7b..00000000 --- a/src/main/java/uk/gov/hmcts/cp/subscription/services/exceptions/CallbackUrlDeliveryException.java +++ /dev/null @@ -1,16 +0,0 @@ -package uk.gov.hmcts.cp.subscription.services.exceptions; - -/** - * Thrown when callbackUrl delivery fails (network error or non-2xx response). Triggers retry via @Retryable. - */ -public class CallbackUrlDeliveryException extends RuntimeException { - private static final long serialVersionUID = 1L; - - public CallbackUrlDeliveryException(final String message) { - super(message); - } - - public CallbackUrlDeliveryException(final String message, final Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index bc56655e..fd95a9d8 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -21,6 +21,11 @@ material-client: intervalMilliSecs: 5000 timeoutMilliSecs: 30000 +callback-client: + retry: + intervalMilliSecs: 1000 + timeoutMilliSecs: 10000 + document-service: url: ${DOCUMENT_SERVICE_URL:http://localhost:8082} diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/config/SSLTrustingRestTemplateConfig.java b/src/test/java/uk/gov/hmcts/cp/subscription/config/SSLTrustingRestTemplateConfig.java new file mode 100644 index 00000000..6d635c90 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/subscription/config/SSLTrustingRestTemplateConfig.java @@ -0,0 +1,43 @@ +package uk.gov.hmcts.cp.subscription.config; + +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Configuration; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +@Configuration +public class SSLTrustingRestTemplateConfig { + + private static final X509TrustManager TRUST_ALL = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (chain == null || chain.length == 0) { + throw new CertificateException("No server certificates"); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + + @PostConstruct + public void trustAllSsl() throws Exception { + var ssl = SSLContext.getInstance("TLS"); + ssl.init(null, new TrustManager[]{TRUST_ALL}, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(ssl.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); + } +} diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandlerTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandlerTest.java index 08a8e09d..59792ab1 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandlerTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/controllers/GlobalExceptionHandlerTest.java @@ -79,7 +79,7 @@ void unsupported_operation_should_handle_ok() { } @Test - void condition_timeout_should_handle_ok() { + void condition_timeout_material_should_handle_ok() { ConditionTimeoutException e = new ConditionTimeoutException("timed out"); ResponseEntity response = @@ -88,6 +88,16 @@ void condition_timeout_should_handle_ok() { assertErrorFields(response, HttpStatus.GATEWAY_TIMEOUT, "Material metadata not ready"); } + @Test + void condition_timeout_callback_should_handle_ok() { + ConditionTimeoutException e = new ConditionTimeoutException("Callback is not ready"); + + ResponseEntity response = + globalExceptionHandler.handleConditionTimeout(e); + + assertErrorFields(response, HttpStatus.GATEWAY_TIMEOUT, "Callback is not ready"); + } + private void assertErrorFields(ResponseEntity errorResponse, HttpStatusCode httpStatusCode, String message) { assertThat(errorResponse.getStatusCode()).isEqualTo(httpStatusCode); assertThat(errorResponse.getBody()).isEqualTo(message); diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/IntegrationTestBase.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/IntegrationTestBase.java index 5fee2a0e..bb1d2ccb 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/integration/IntegrationTestBase.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/IntegrationTestBase.java @@ -8,6 +8,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; + import uk.gov.hmcts.cp.openapi.model.ClientSubscriptionRequest; import uk.gov.hmcts.cp.openapi.model.NotificationEndpoint; import uk.gov.hmcts.cp.subscription.integration.config.TestContainersInitialise; @@ -32,6 +33,11 @@ @Slf4j public abstract class IntegrationTestBase { + protected static final UUID MATERIAL_ID_TIMEOUT = UUID.fromString("11111111-1111-1111-1111-111111111112"); + protected static final String NOTIFICATIONS_PCR_URI = "/notifications/pcr"; + protected static final String CLIENT_SUBSCRIPTIONS_URI = "/client-subscriptions"; + protected static final String CALLBACK_URI = "/callback/notify"; + @Resource protected MockMvc mockMvc; diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerIntegrationTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerIntegrationTest.java index fc8b0516..3d8f2da3 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerIntegrationTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerIntegrationTest.java @@ -3,7 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; -import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.wiremock.spring.ConfigureWireMock; import org.wiremock.spring.EnableWireMock; @@ -31,11 +31,8 @@ import uk.gov.hmcts.cp.subscription.services.CallbackDeliveryService; -@EnableWireMock({@ConfigureWireMock(name = "material-client", baseUrlProperties = "material-client.url", port = 18081)}) -@TestPropertySource(properties = { - "material-client.retry.timeoutMilliSecs=500", - "material-client.retry.intervalMilliSecs=100" -}) +@EnableWireMock({@ConfigureWireMock(name = "material-client", baseUrlProperties = "material-client.url", port = 0)}) +@ActiveProfiles("test") class NotificationControllerIntegrationTest extends IntegrationTestBase { private static final String NOTIFICATION_PCR_URI = "/notifications/pcr"; @@ -54,7 +51,7 @@ void setUp() { @Test void prison_court_register_generated_should_return_success() throws Exception { - String pcrPayload = loadPayload("stubs/requests/pcr-request-prison-court-register.json"); + String pcrPayload = loadPayload("stubs/requests/progression/pcr-request-prison-court-register.json"); mockMvc.perform(post(NOTIFICATION_PCR_URI) .contentType(MediaType.APPLICATION_JSON) @@ -68,7 +65,7 @@ void prison_court_register_generated_should_return_success() throws Exception { @Test void custodial_result_should_return_unsupported() throws Exception { - String pcrPayload = loadPayload("stubs/requests/pcr-request-custodial-result.json"); + String pcrPayload = loadPayload("stubs/requests/progression/pcr-request-custodial-result.json"); doThrow(new UnsupportedOperationException("CUSTODIAL_RESULT not implemented")) .when(callbackDeliveryService).processPcrEvent(any(PcrEventPayload.class), any(UUID.class)); @@ -86,7 +83,7 @@ void custodial_result_should_return_unsupported() throws Exception { @Test void material_metadata_not_found_should_return_404() throws Exception { - String pcrPayload = loadPayload("stubs/requests/pcr-request-material-not-found.json"); + String pcrPayload = loadPayload("stubs/requests/progression/pcr-request-material-not-found.json"); mockMvc.perform(post(NOTIFICATION_PCR_URI) .contentType(MediaType.APPLICATION_JSON) @@ -98,7 +95,8 @@ void material_metadata_not_found_should_return_404() throws Exception { @Test void material_metadata_timeout_should_return_504_via_global_exception_handler() throws Exception { - String pcrPayload = loadPayload("stubs/requests/pcr-request-material-timeout.json"); + String pcrPayload = loadPayload("stubs/requests/progression/pcr-request-material-timeout.json"); + mockMvc.perform(post(NOTIFICATION_PCR_URI) .contentType(MediaType.APPLICATION_JSON) diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerValidationTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerValidationTest.java index 9b3d5657..cbcb09a2 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerValidationTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/NotificationControllerValidationTest.java @@ -28,9 +28,9 @@ class NotificationControllerValidationTest extends IntegrationTestBase { private static final String NOTIFICATION_PCR_URI = "/notifications/pcr"; - private static final String PCR_REQUEST_VALID = "stubs/requests/pcr-request-valid.json"; - private static final String PCR_REQUEST_MISSING_MATERIAL = "stubs/requests/pcr-request-missing-material.json"; - private static final String PCR_REQUEST_MISSING_EVENT = "stubs/requests/pcr-request-missing-event.json"; + private static final String PCR_REQUEST_VALID = "stubs/requests/progression/pcr-request-valid.json"; + private static final String PCR_REQUEST_MISSING_MATERIAL = "stubs/requests/progression/pcr-request-missing-material.json"; + private static final String PCR_REQUEST_MISSING_EVENT = "stubs/requests/progression/pcr-request-missing-event.json"; private static final UUID SUBSCRIPTION_ID = randomUUID(); private static final UUID DOCUMENT_ID = randomUUID(); private static final String SUBSCRIPTION_DOCUMENT_URI = "/client-subscriptions/{clientSubscriptionId}/documents/{documentId}"; diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionControllerValidationTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionControllerValidationTest.java index 1fff37fd..21611a7d 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionControllerValidationTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionControllerValidationTest.java @@ -11,8 +11,8 @@ class SubscriptionControllerValidationTest extends IntegrationTestBase { private static final String CLIENT_SUBSCRIPTIONS = "/client-subscriptions"; - private static final String SUBSCRIPTION_REQUEST_BAD_EVENT = "stubs/requests/subscription-request-bad-event-type.json"; - private static final String SUBSCRIPTION_REQUEST_INVALID_CALLBACK = "stubs/requests/subscription-request-invalid-callback-url.json"; + private static final String SUBSCRIPTION_REQUEST_BAD_EVENT = "stubs/requests/subscription/subscription-request-bad-event-type.json"; + private static final String SUBSCRIPTION_REQUEST_INVALID_CALLBACK = "stubs/requests/subscription/subscription-request-invalid-callback-url.json"; @Test void bad_event_type_should_return_400() throws Exception { diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionSaveControllerIntegrationTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionSaveControllerIntegrationTest.java index 04011f2d..f44dec3b 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionSaveControllerIntegrationTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionSaveControllerIntegrationTest.java @@ -17,7 +17,7 @@ class SubscriptionSaveControllerIntegrationTest extends IntegrationTestBase { - private static final String SUBSCRIPTION_REQUEST_VALID = "stubs/requests/subscription-request-valid.json"; + private static final String SUBSCRIPTION_REQUEST_VALID = "stubs/requests/subscription/subscription-request-valid.json"; @BeforeEach void beforeEach() { diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionUpdateControllerIntegrationTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionUpdateControllerIntegrationTest.java index 0f1cd909..8b919d72 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionUpdateControllerIntegrationTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/controllers/SubscriptionUpdateControllerIntegrationTest.java @@ -22,7 +22,7 @@ @Slf4j class SubscriptionUpdateControllerIntegrationTest extends IntegrationTestBase { - private static final String SUBSCRIPTION_REQUEST_VALID = "stubs/requests/subscription-request-valid.json"; + private static final String SUBSCRIPTION_REQUEST_VALID = "stubs/requests/subscription/subscription-request-valid.json"; @BeforeEach void beforeEach() { diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/e2e/NotificationPCRE2EIntegrationTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/e2e/NotificationPCRE2EIntegrationTest.java new file mode 100644 index 00000000..55e0b458 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/e2e/NotificationPCRE2EIntegrationTest.java @@ -0,0 +1,223 @@ +package uk.gov.hmcts.cp.subscription.integration.e2e; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.WireMock; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.wiremock.spring.ConfigureWireMock; +import org.wiremock.spring.EnableWireMock; +import org.wiremock.spring.InjectWireMock; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import uk.gov.hmcts.cp.material.openapi.api.MaterialApi; +import uk.gov.hmcts.cp.subscription.config.SSLTrustingRestTemplateConfig; +import uk.gov.hmcts.cp.subscription.integration.IntegrationTestBase; + +import static uk.gov.hmcts.cp.subscription.integration.stubs.CallbackStub.getDocumentIdFromCallbackServeEvents; +import static uk.gov.hmcts.cp.subscription.integration.stubs.CallbackStub.stubCallbackEndpoint; +import static uk.gov.hmcts.cp.subscription.integration.stubs.CallbackStub.stubCallbackEndpointReturnsServerError; +import static uk.gov.hmcts.cp.subscription.integration.stubs.MaterialStub.stubMaterialBinary; +import static uk.gov.hmcts.cp.subscription.integration.stubs.MaterialStub.stubMaterialContent; +import static uk.gov.hmcts.cp.subscription.integration.stubs.MaterialStub.stubMaterialMetadata; +import static uk.gov.hmcts.cp.subscription.integration.stubs.MaterialStub.stubMaterialMetadataNoContent; + +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.moreThanOrExactly; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Objects.nonNull; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; + +import org.springframework.test.web.servlet.ResultActions; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@EnableWireMock({ + @ConfigureWireMock(name = "material-client", baseUrlProperties = "material-client.url", port = 0), + @ConfigureWireMock(name = "callback-client", httpsBaseUrlProperties = "callback-client.url", httpsPort = 0) +}) +@ActiveProfiles("test") +@Import(SSLTrustingRestTemplateConfig.class) +class NotificationPcrE2EIntegrationTest extends IntegrationTestBase { + + private UUID subscriptionId; + private UUID callbackDocumentId; + private static final UUID MATERIAL_ID = UUID.fromString("6c198796-08bb-4803-b456-fa0c29ca6021"); + private static final String DOCUMENT_URI = CLIENT_SUBSCRIPTIONS_URI + "/{clientSubscriptionId}/documents/{documentId}"; + private static final String SUBSCRIPTION_REQUEST_E2E = "stubs/requests/subscription/subscription-pcr-request.json"; + private static final String PCR_EVENT_PAYLOAD_PATH = "stubs/requests/progression/pcr-request-prison-court-register.json"; + private static final String PCR_EVENT_TIMEOUT_PATH = "stubs/requests/progression/pcr-request-material-timeout.json"; + + @InjectWireMock("callback-client") + private WireMockServer callbackWireMock; + + @Value("${callback-client.url}") + private String callbackBaseUrl; + + @MockitoSpyBean + private MaterialApi materialApi; + + @BeforeEach + void setUp() { + WireMock.reset(); + if (nonNull(callbackWireMock)) { + callbackWireMock.resetAll(); + } + clearAllTables(); + } + + @Test + void should_document_retrieval_success() throws Exception { + given_i_am_a_subscriber_with_a_subscription(); + given_i_have_a_callback_endpoint(); + given_material_service_returns_document_success(); + + when_a_pcr_event_is_posted(); + when_material_service_responds(); + + then_the_subscriber_receives_a_callback(); + then_the_subscriber_can_retrieve_the_document(); + } + + @Test + void should_document_retrieval_failure() throws Exception { + given_i_am_a_subscriber_with_a_subscription(); + given_i_have_a_callback_endpoint(); + given_material_service_returns_document_not_found(); + + when_a_pcr_event_is_posted_with_timeout(); + when_material_service_responds(); + + then_the_material_api_was_polled(); + then_the_subscriber_does_not_receive_a_callback(); + } + + @Test + void should_return_504_when_callback_client_does_not_respond() throws Exception { + given_i_am_a_subscriber_with_a_subscription(); + given_callback_endpoint_returns_server_error(); + given_material_service_returns_document_success(); + + when_a_pcr_event_is_posted_expect_callback_delivery_timeout(); + + then_callback_was_attempted(); + } + + private void given_i_am_a_subscriber_with_a_subscription() throws Exception { + createSubscription(); + } + + private void given_i_have_a_callback_endpoint() throws IOException { + stubCallbackEndpoint(callbackWireMock, CALLBACK_URI); + } + + private void given_material_service_returns_document_success() throws IOException { + stubMaterialMetadata(MATERIAL_ID); + stubMaterialContent(MATERIAL_ID); + stubMaterialBinary(MATERIAL_ID); + } + + private void given_material_service_returns_document_not_found() { + stubMaterialMetadataNoContent(MATERIAL_ID_TIMEOUT); + } + + private void given_callback_endpoint_returns_server_error() { + stubCallbackEndpointReturnsServerError(callbackWireMock, CALLBACK_URI); + } + + private void when_a_pcr_event_is_posted_expect_callback_delivery_timeout() throws Exception { + postPcrEvent(PCR_EVENT_PAYLOAD_PATH) + .andExpect(status().isGatewayTimeout()) + .andExpect(content().string("Callback is not ready")); + } + + private void then_callback_was_attempted() { + callbackWireMock.verify(moreThanOrExactly(1), postRequestedFor(urlPathEqualTo(CALLBACK_URI))); + } + + private void when_a_pcr_event_is_posted() throws Exception { + postPcrEvent(PCR_EVENT_PAYLOAD_PATH).andExpect(status().isAccepted()); + } + + private void when_a_pcr_event_is_posted_with_timeout() throws Exception { + postPcrEvent(PCR_EVENT_TIMEOUT_PATH) + .andExpect(status().isGatewayTimeout()) + .andExpect(content().string("Material metadata not ready")); + } + + private ResultActions postPcrEvent(String payloadPath) throws Exception { + return mockMvc.perform(post(NOTIFICATIONS_PCR_URI) + .contentType(MediaType.APPLICATION_JSON) + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .content(loadPayload(payloadPath))) + .andDo(print()); + } + + private void when_material_service_responds() { + await().atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> verify(materialApi, atLeastOnce()).getMaterialMetadataByMaterialId(any(UUID.class))); + } + + private void then_the_material_api_was_polled() { + verify(materialApi, atLeastOnce()).getMaterialMetadataByMaterialId(eq(MATERIAL_ID_TIMEOUT)); + } + + private void then_the_subscriber_receives_a_callback() { + callbackWireMock.verify(1, postRequestedFor(urlPathEqualTo(CALLBACK_URI))); + callbackDocumentId = getDocumentIdFromCallbackServeEvents(callbackWireMock, CALLBACK_URI); + } + + private void then_the_subscriber_does_not_receive_a_callback() { + callbackWireMock.verify(0, postRequestedFor(urlPathEqualTo(CALLBACK_URI))); + } + + private void then_the_subscriber_can_retrieve_the_document() throws Exception { + getDocumentAndExpectPdf(subscriptionId, callbackDocumentId); + } + + private void getDocumentAndExpectPdf(UUID subId, UUID docId) throws Exception { + mockMvc.perform(get(DOCUMENT_URI, subId, docId)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", org.hamcrest.Matchers.containsString("application/pdf"))) + .andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("PrisonCourtRegister"))) + .andExpect(content().contentType(MediaType.APPLICATION_PDF)); + } + + private void createSubscription() throws Exception { + String callbackUrl = callbackBaseUrl.endsWith("/") ? callbackBaseUrl + CALLBACK_URI.substring(1) : callbackBaseUrl + CALLBACK_URI; + String body = loadPayload(SUBSCRIPTION_REQUEST_E2E).replace("{{callback.url}}", callbackUrl); + String json = postSubscriptionAndReturnJson(body); + subscriptionId = UUID.fromString(new ObjectMapper().readTree(json).get("clientSubscriptionId").asText()); + } + + private String postSubscriptionAndReturnJson(String body) throws Exception { + return mockMvc.perform(post(CLIENT_SUBSCRIPTIONS_URI) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.clientSubscriptionId").exists()) + .andReturn().getResponse().getContentAsString(); + } +} diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/stubs/CallbackStub.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/stubs/CallbackStub.java new file mode 100644 index 00000000..2ef4ff97 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/stubs/CallbackStub.java @@ -0,0 +1,67 @@ +package uk.gov.hmcts.cp.subscription.integration.stubs; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.stubbing.ServeEvent; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Objects.nonNull; + +/** + * WireMock stub for the callback endpoint (event notification delivery). + * Use with the callback-client WireMock server (e.g. from @EnableWireMock). + */ +public final class CallbackStub { + + private static final String CALLBACK_RESPONSE_PATH = "wiremock/callback-client/files/callback-accepted.json"; + private static final String APPLICATION_JSON = "application/json"; + private static final String CONTENT_TYPE = "Content-Type"; + + private CallbackStub() { + } + + public static void stubCallbackEndpoint(WireMockServer server, String callbackUri) throws IOException { + String body = new ClassPathResource(CALLBACK_RESPONSE_PATH).getContentAsString(StandardCharsets.UTF_8); + server.stubFor(post(urlPathEqualTo(callbackUri)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(body))); + } + + public static void stubCallbackEndpointReturnsServerError(WireMockServer server, String callbackUri) { + server.stubFor(post(urlPathEqualTo(callbackUri)) + .willReturn(aResponse().withStatus(500))); + } + + public static UUID getDocumentIdFromCallbackServeEvents(WireMockServer server, String callbackUri) { + return server.getAllServeEvents().stream() + .map(ServeEvent::getRequest) + .filter(r -> nonNull(r.getUrl()) && r.getUrl().contains(callbackUri)) + .map(r -> parseDocumentIdFromBody(r.getBodyAsString())) + .findFirst() + .orElseThrow(() -> + new AssertionError("Callback request body did not contain documentId")); + } + + private static UUID parseDocumentIdFromBody(String body) { + if (body.isEmpty()) return null; + try { + return Optional.ofNullable(new ObjectMapper().readTree(body).get("documentId")) + .map(JsonNode::asText) + .map(UUID::fromString) + .orElse(null); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/integration/stubs/MaterialStub.java b/src/test/java/uk/gov/hmcts/cp/subscription/integration/stubs/MaterialStub.java new file mode 100644 index 00000000..9febb958 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/subscription/integration/stubs/MaterialStub.java @@ -0,0 +1,64 @@ +package uk.gov.hmcts.cp.subscription.integration.stubs; + +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; + +public final class MaterialStub { + + private static final String MATERIAL_METADATA_RESPONSE_PATH = "wiremock/material-client/files/material-response.json"; + private static final String MATERIAL_CONTENT_RESPONSE_PATH = "wiremock/material-client/files/material-with-contenturl.json"; + private static final String MATERIAL_PDF_PATH = "wiremock/material-client/files/material-content.pdf"; + private static final String MATERIAL_URI = "/material-query-api/query/api/rest/material/material/"; + private static final String METADATA = "/metadata"; + private static final String APPLICATION_JSON = "application/json"; + private static final String CONTENT_TYPE = "Content-Type"; + + private MaterialStub() { + } + + public static void stubMaterialMetadata(UUID materialId) throws IOException { + String materialPath = MATERIAL_URI + materialId; + String metadataBody = new ClassPathResource(MATERIAL_METADATA_RESPONSE_PATH).getContentAsString(StandardCharsets.UTF_8); + stubFor(get(urlPathMatching(".*" + materialPath + METADATA)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(metadataBody))); + } + + public static void stubMaterialContent(UUID materialId) throws IOException { + String materialPath = MATERIAL_URI + materialId; + String contentBody = new ClassPathResource(MATERIAL_CONTENT_RESPONSE_PATH).getContentAsString(StandardCharsets.UTF_8); + stubFor(get(urlPathMatching(".*" + materialPath + "/content")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, "application/vnd.material.query.material+json") + .withBody(contentBody) + .withTransformers("response-template"))); + } + + public static void stubMaterialBinary(UUID materialId) throws IOException { + String materialPath = MATERIAL_URI + materialId; + byte[] pdfBody = new ClassPathResource(MATERIAL_PDF_PATH).getContentAsByteArray(); + stubFor(get(urlPathMatching(".*" + materialPath + "/binary")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, "application/pdf") + .withHeader("Content-Disposition", "inline; filename=\"material-content.pdf\"") + .withBody(pdfBody))); + } + + public static void stubMaterialMetadataNoContent(UUID materialId) { + String materialPath = MATERIAL_URI + materialId; + stubFor(get(urlPathMatching(".*" + materialPath + METADATA)) + .willReturn(aResponse().withStatus(204))); + } +} diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/services/CallbackServiceTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/services/CallbackServiceTest.java index ddf21e0e..e44c7b41 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/services/CallbackServiceTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/services/CallbackServiceTest.java @@ -5,23 +5,51 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.client.RestClientException; import uk.gov.hmcts.cp.openapi.model.EventNotificationPayload; import uk.gov.hmcts.cp.subscription.clients.CallbackClient; +import uk.gov.hmcts.cp.subscription.config.AppProperties; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class CallbackServiceTest { + + @Mock + private AppProperties appProperties; + @Mock - CallbackClient callbackClient; + private CallbackClient callbackClient; @InjectMocks - CallbackService callbackService; + private CallbackService callbackService; @Test void send_to_subscriber_should_post() { + when(appProperties.getCallbackRetryIntervalMilliSecs()).thenReturn(10); + when(appProperties.getCallbackRetryTimeoutMilliSecs()).thenReturn(1000); + EventNotificationPayload eventNotificationPayload = EventNotificationPayload.builder().build(); callbackService.sendToSubscriber("url", eventNotificationPayload); + verify(callbackClient).sendNotification("url", eventNotificationPayload); } + + @Test + void send_to_subscriber_should_retry_until_success() { + when(appProperties.getCallbackRetryIntervalMilliSecs()).thenReturn(10); + when(appProperties.getCallbackRetryTimeoutMilliSecs()).thenReturn(500); + EventNotificationPayload payload = EventNotificationPayload.builder().build(); + + doThrow(new RestClientException("intermittent failure")) + .doNothing() + .when(callbackClient).sendNotification("url", payload); + + callbackService.sendToSubscriber("url", payload); + + verify(callbackClient, times(2)).sendNotification("url", payload); + } } \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/unit/controllers/NotificationControllerTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/unit/controllers/NotificationControllerTest.java index b5935a2f..3388c023 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/unit/controllers/NotificationControllerTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/unit/controllers/NotificationControllerTest.java @@ -1,6 +1,5 @@ package uk.gov.hmcts.cp.subscription.unit.controllers; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -13,9 +12,7 @@ import uk.gov.hmcts.cp.subscription.controllers.NotificationController; import uk.gov.hmcts.cp.subscription.managers.NotificationManager; import uk.gov.hmcts.cp.subscription.model.DocumentContent; -import uk.gov.hmcts.cp.subscription.services.exceptions.CallbackUrlDeliveryException; -import java.net.URISyntaxException; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -71,40 +68,21 @@ void runtime_exception_should_propagate() { @SneakyThrows @Test - void json_processing_exception_in_callback_should_throw_callback_url_delivery_exception() { + void exception_from_manager_should_propagate_for_global_handler() { PcrEventPayload payload = PcrEventPayload.builder() .materialId(MATERIAL_ID) .eventType(EventType.PRISON_COURT_REGISTER_GENERATED) .build(); - JsonProcessingException cause = new JsonProcessingException("Invalid JSON") {}; + RuntimeException failure = new RuntimeException("Callback delivery failed"); - doThrow(new CallbackUrlDeliveryException("PCR - Failed to build or deliver callback payload: " + cause.getMessage(), cause)) + doThrow(failure) .when(notificationManager).processPcrNotification(any(PcrEventPayload.class)); - CallbackUrlDeliveryException thrown = assertThrows(CallbackUrlDeliveryException.class, + RuntimeException thrown = assertThrows(RuntimeException.class, () -> notificationController.createNotificationPCR(payload)); - assertThat(thrown.getMessage()).contains("PCR - Failed to build or deliver callback payload"); - assertThat(thrown.getCause()).isEqualTo(cause); - } - - @SneakyThrows - @Test - void uri_syntax_exception_in_callback_should_throw_callback_url_delivery_exception() { - PcrEventPayload payload = PcrEventPayload.builder() - .materialId(MATERIAL_ID) - .eventType(EventType.PRISON_COURT_REGISTER_GENERATED) - .build(); - URISyntaxException cause = new URISyntaxException("invalid", "bad uri"); - - doThrow(new CallbackUrlDeliveryException("PCR - Failed to build or deliver callback payload: " + cause.getMessage(), cause)) - .when(notificationManager).processPcrNotification(any(PcrEventPayload.class)); - - CallbackUrlDeliveryException thrown = assertThrows(CallbackUrlDeliveryException.class, - () -> notificationController.createNotificationPCR(payload)); - - assertThat(thrown.getMessage()).contains("PCR - Failed to build or deliver callback payload"); - assertThat(thrown.getCause()).isEqualTo(cause); + assertThat(thrown).isSameAs(failure); + verify(notificationManager).processPcrNotification(eq(payload)); } @Test diff --git a/src/test/java/uk/gov/hmcts/cp/subscription/unit/managers/NotificationManagerTest.java b/src/test/java/uk/gov/hmcts/cp/subscription/unit/managers/NotificationManagerTest.java index ed6df33e..7e5e64e9 100644 --- a/src/test/java/uk/gov/hmcts/cp/subscription/unit/managers/NotificationManagerTest.java +++ b/src/test/java/uk/gov/hmcts/cp/subscription/unit/managers/NotificationManagerTest.java @@ -1,6 +1,5 @@ package uk.gov.hmcts.cp.subscription.unit.managers; -import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,9 +17,7 @@ import uk.gov.hmcts.cp.subscription.services.DocumentService; import uk.gov.hmcts.cp.subscription.services.NotificationService; import uk.gov.hmcts.cp.subscription.services.SubscriptionService; -import uk.gov.hmcts.cp.subscription.services.exceptions.CallbackUrlDeliveryException; -import java.net.URISyntaxException; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 00000000..509fc05d --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,4 @@ +material-client.retry.timeoutMilliSecs=500 +material-client.retry.intervalMilliSecs=100 +callback-client.retry.timeoutMilliSecs=5000 +callback-client.retry.intervalMilliSecs=200 \ No newline at end of file diff --git a/src/test/resources/mappings/material-content-full-mapping.json b/src/test/resources/mappings/material-content-full-mapping.json deleted file mode 100644 index 4ea452ec..00000000 --- a/src/test/resources/mappings/material-content-full-mapping.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "request": { - "method": "GET", - "urlPathPattern": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/content" - }, - "response": { - "status": 200, - "headers": { - "Content-Type": "application/vnd.material.query.material+json" - }, - "bodyFileName": "material-with-contenturl.json" - } -} diff --git a/src/test/resources/stubs/requests/pcr-request-custodial-result.json b/src/test/resources/stubs/requests/progression/pcr-request-custodial-result.json similarity index 100% rename from src/test/resources/stubs/requests/pcr-request-custodial-result.json rename to src/test/resources/stubs/requests/progression/pcr-request-custodial-result.json diff --git a/src/test/resources/stubs/requests/pcr-request-material-not-found.json b/src/test/resources/stubs/requests/progression/pcr-request-material-not-found.json similarity index 100% rename from src/test/resources/stubs/requests/pcr-request-material-not-found.json rename to src/test/resources/stubs/requests/progression/pcr-request-material-not-found.json diff --git a/src/test/resources/stubs/requests/pcr-request-material-timeout.json b/src/test/resources/stubs/requests/progression/pcr-request-material-timeout.json similarity index 100% rename from src/test/resources/stubs/requests/pcr-request-material-timeout.json rename to src/test/resources/stubs/requests/progression/pcr-request-material-timeout.json diff --git a/src/test/resources/stubs/requests/pcr-request-missing-event.json b/src/test/resources/stubs/requests/progression/pcr-request-missing-event.json similarity index 100% rename from src/test/resources/stubs/requests/pcr-request-missing-event.json rename to src/test/resources/stubs/requests/progression/pcr-request-missing-event.json diff --git a/src/test/resources/stubs/requests/pcr-request-missing-material.json b/src/test/resources/stubs/requests/progression/pcr-request-missing-material.json similarity index 100% rename from src/test/resources/stubs/requests/pcr-request-missing-material.json rename to src/test/resources/stubs/requests/progression/pcr-request-missing-material.json diff --git a/src/test/resources/stubs/requests/pcr-request-prison-court-register.json b/src/test/resources/stubs/requests/progression/pcr-request-prison-court-register.json similarity index 100% rename from src/test/resources/stubs/requests/pcr-request-prison-court-register.json rename to src/test/resources/stubs/requests/progression/pcr-request-prison-court-register.json diff --git a/src/test/resources/stubs/requests/pcr-request-valid.json b/src/test/resources/stubs/requests/progression/pcr-request-valid.json similarity index 100% rename from src/test/resources/stubs/requests/pcr-request-valid.json rename to src/test/resources/stubs/requests/progression/pcr-request-valid.json diff --git a/src/test/resources/stubs/requests/subscription/subscription-pcr-request.json b/src/test/resources/stubs/requests/subscription/subscription-pcr-request.json new file mode 100644 index 00000000..f3e9fe87 --- /dev/null +++ b/src/test/resources/stubs/requests/subscription/subscription-pcr-request.json @@ -0,0 +1,8 @@ +{ + "notificationEndpoint": { + "callbackUrl": "{{callback.url}}" + }, + "eventTypes": [ + "PRISON_COURT_REGISTER_GENERATED" + ] +} diff --git a/src/test/resources/stubs/requests/subscription-request-bad-event-type.json b/src/test/resources/stubs/requests/subscription/subscription-request-bad-event-type.json similarity index 100% rename from src/test/resources/stubs/requests/subscription-request-bad-event-type.json rename to src/test/resources/stubs/requests/subscription/subscription-request-bad-event-type.json diff --git a/src/test/resources/stubs/requests/subscription-request-invalid-callback-url.json b/src/test/resources/stubs/requests/subscription/subscription-request-invalid-callback-url.json similarity index 100% rename from src/test/resources/stubs/requests/subscription-request-invalid-callback-url.json rename to src/test/resources/stubs/requests/subscription/subscription-request-invalid-callback-url.json diff --git a/src/test/resources/stubs/requests/subscription-request-valid.json b/src/test/resources/stubs/requests/subscription/subscription-request-valid.json similarity index 100% rename from src/test/resources/stubs/requests/subscription-request-valid.json rename to src/test/resources/stubs/requests/subscription/subscription-request-valid.json diff --git a/src/test/resources/wiremock/callback-client/files/callback-accepted.json b/src/test/resources/wiremock/callback-client/files/callback-accepted.json new file mode 100644 index 00000000..4228b900 --- /dev/null +++ b/src/test/resources/wiremock/callback-client/files/callback-accepted.json @@ -0,0 +1,3 @@ +{ + "status": "accepted" +} \ No newline at end of file diff --git a/src/test/resources/__files/material-content.pdf b/src/test/resources/wiremock/material-client/files/material-content.pdf similarity index 100% rename from src/test/resources/__files/material-content.pdf rename to src/test/resources/wiremock/material-client/files/material-content.pdf diff --git a/src/test/resources/__files/material-response.json b/src/test/resources/wiremock/material-client/files/material-response.json similarity index 99% rename from src/test/resources/__files/material-response.json rename to src/test/resources/wiremock/material-client/files/material-response.json index 30f20a05..9baf7c5b 100644 --- a/src/test/resources/__files/material-response.json +++ b/src/test/resources/wiremock/material-client/files/material-response.json @@ -4,4 +4,4 @@ "fileName": "PrisonCourtRegister_20251219083322.pdf", "mimeType": "application/pdf", "materialAddedDate": "2025-12-19T08:33:29.866Z" -} \ No newline at end of file +} diff --git a/src/test/resources/__files/material-with-contenturl.json b/src/test/resources/wiremock/material-client/files/material-with-contenturl.json similarity index 59% rename from src/test/resources/__files/material-with-contenturl.json rename to src/test/resources/wiremock/material-client/files/material-with-contenturl.json index f0b007a2..f8832eb9 100644 --- a/src/test/resources/__files/material-with-contenturl.json +++ b/src/test/resources/wiremock/material-client/files/material-with-contenturl.json @@ -5,5 +5,5 @@ "fileName": "PrisonCourtRegister_20251219083322.pdf", "mimeType": "application/pdf" }, - "contentUrl": "http://localhost:18081/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/binary" + "contentUrl": "http://{{request.host}}:{{request.port}}/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/binary" } diff --git a/src/test/resources/mappings/material-content-mapping.json b/src/test/resources/wiremock/material-client/mappings/material-binary-mapping.json similarity index 87% rename from src/test/resources/mappings/material-content-mapping.json rename to src/test/resources/wiremock/material-client/mappings/material-binary-mapping.json index 89d431bb..f12bf4a6 100644 --- a/src/test/resources/mappings/material-content-mapping.json +++ b/src/test/resources/wiremock/material-client/mappings/material-binary-mapping.json @@ -1,7 +1,7 @@ { "request": { "method": "GET", - "url": "/material-query-api/query/api/rest/material/material/7c198796-08bb-4803-b456-fa0c29ca6021/content" + "url": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/binary" }, "response": { "status": 200, diff --git a/src/test/resources/wiremock/material-client/mappings/material-content-full-mapping.json b/src/test/resources/wiremock/material-client/mappings/material-content-full-mapping.json new file mode 100644 index 00000000..fcf1ab05 --- /dev/null +++ b/src/test/resources/wiremock/material-client/mappings/material-content-full-mapping.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "GET", + "url": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/content" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.material.query.material+json" + }, + "bodyFileName": "material-with-contenturl.json", + "transformers": [ + "response-template" + ] + } +} diff --git a/src/test/resources/mappings/material-metadata-mapping.json b/src/test/resources/wiremock/material-client/mappings/material-metadata-mapping.json similarity index 100% rename from src/test/resources/mappings/material-metadata-mapping.json rename to src/test/resources/wiremock/material-client/mappings/material-metadata-mapping.json diff --git a/src/test/resources/mappings/material-metadata-timeout-mapping.json b/src/test/resources/wiremock/material-client/mappings/material-metadata-timeout-mapping.json similarity index 100% rename from src/test/resources/mappings/material-metadata-timeout-mapping.json rename to src/test/resources/wiremock/material-client/mappings/material-metadata-timeout-mapping.json diff --git a/stubs/material-client/__files/material-content.pdf b/stubs/material-client/__files/material-content.pdf new file mode 100644 index 00000000..94e3be39 Binary files /dev/null and b/stubs/material-client/__files/material-content.pdf differ diff --git a/stubs/material-client/__files/material-response.json b/stubs/material-client/__files/material-response.json new file mode 100644 index 00000000..9baf7c5b --- /dev/null +++ b/stubs/material-client/__files/material-response.json @@ -0,0 +1,7 @@ +{ + "materialId": "6c198796-08bb-4803-b456-fa0c29ca6021", + "alfrescoAssetId": "82257b1b-571d-432e-8871-b0c5b4bd18b1", + "fileName": "PrisonCourtRegister_20251219083322.pdf", + "mimeType": "application/pdf", + "materialAddedDate": "2025-12-19T08:33:29.866Z" +} diff --git a/stubs/material-client/__files/material-with-contenturl.json b/stubs/material-client/__files/material-with-contenturl.json new file mode 100644 index 00000000..f8832eb9 --- /dev/null +++ b/stubs/material-client/__files/material-with-contenturl.json @@ -0,0 +1,9 @@ +{ + "materialId": "6c198796-08bb-4803-b456-fa0c29ca6021", + "metadata": { + "materialId": "6c198796-08bb-4803-b456-fa0c29ca6021", + "fileName": "PrisonCourtRegister_20251219083322.pdf", + "mimeType": "application/pdf" + }, + "contentUrl": "http://{{request.host}}:{{request.port}}/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/binary" +} diff --git a/src/test/resources/mappings/material-binary-mapping.json b/stubs/material-client/mappings/material-binary-mapping.json similarity index 68% rename from src/test/resources/mappings/material-binary-mapping.json rename to stubs/material-client/mappings/material-binary-mapping.json index 9bb54e7c..f12bf4a6 100644 --- a/src/test/resources/mappings/material-binary-mapping.json +++ b/stubs/material-client/mappings/material-binary-mapping.json @@ -1,7 +1,7 @@ { "request": { "method": "GET", - "urlPathPattern": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/binary" + "url": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/binary" }, "response": { "status": 200, diff --git a/stubs/material-client/mappings/material-content-full-mapping.json b/stubs/material-client/mappings/material-content-full-mapping.json new file mode 100644 index 00000000..fcf1ab05 --- /dev/null +++ b/stubs/material-client/mappings/material-content-full-mapping.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "GET", + "url": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/content" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.material.query.material+json" + }, + "bodyFileName": "material-with-contenturl.json", + "transformers": [ + "response-template" + ] + } +} diff --git a/src/test/resources/mappings/material-metadata-not-found-mapping.json b/stubs/material-client/mappings/material-metadata-mapping.json similarity index 62% rename from src/test/resources/mappings/material-metadata-not-found-mapping.json rename to stubs/material-client/mappings/material-metadata-mapping.json index ecb33f4d..083403ea 100644 --- a/src/test/resources/mappings/material-metadata-not-found-mapping.json +++ b/stubs/material-client/mappings/material-metadata-mapping.json @@ -1,13 +1,13 @@ { "request": { "method": "GET", - "url": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6022/metadata" + "url": "/material-query-api/query/api/rest/material/material/6c198796-08bb-4803-b456-fa0c29ca6021/metadata" }, "response": { - "status": 404, + "status": 200, "headers": { "Content-Type": "application/json" }, - "body": "{\"error\": \"Material not found\"}" + "bodyFileName": "material-response.json" } } diff --git a/stubs/material-client/mappings/material-metadata-timeout-mapping.json b/stubs/material-client/mappings/material-metadata-timeout-mapping.json new file mode 100644 index 00000000..63ed8524 --- /dev/null +++ b/stubs/material-client/mappings/material-metadata-timeout-mapping.json @@ -0,0 +1,9 @@ +{ + "request": { + "method": "GET", + "url": "/material-query-api/query/api/rest/material/material/11111111-1111-1111-1111-111111111112/metadata" + }, + "response": { + "status": 204 + } +}