diff --git a/src/main/java/uk/nhs/prm/e2etests/model/request/PdsAdaptorRequest.java b/src/main/java/uk/nhs/prm/e2etests/model/request/PdsAdaptorRequest.java index 86aa4ae3..46f76eee 100644 --- a/src/main/java/uk/nhs/prm/e2etests/model/request/PdsAdaptorRequest.java +++ b/src/main/java/uk/nhs/prm/e2etests/model/request/PdsAdaptorRequest.java @@ -1,7 +1,5 @@ package uk.nhs.prm.e2etests.model.request; -import lombok.EqualsAndHashCode; - public record PdsAdaptorRequest(String previousGp, String recordETag) { } \ No newline at end of file diff --git a/src/main/java/uk/nhs/prm/e2etests/property/EhrOutServiceProperties.java b/src/main/java/uk/nhs/prm/e2etests/property/EhrOutServiceProperties.java new file mode 100644 index 00000000..f9b706c0 --- /dev/null +++ b/src/main/java/uk/nhs/prm/e2etests/property/EhrOutServiceProperties.java @@ -0,0 +1,14 @@ +package uk.nhs.prm.e2etests.property; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class EhrOutServiceProperties { + @Value("${aws.configuration.serviceUrls.ehrOutService}") + private String ehrOutServiceUrl; + + public String getEhrOutServiceUrl() { + return ehrOutServiceUrl; + } +} diff --git a/src/main/java/uk/nhs/prm/e2etests/property/NhsProperties.java b/src/main/java/uk/nhs/prm/e2etests/property/NhsProperties.java index 0bcec45c..ac7e4fde 100644 --- a/src/main/java/uk/nhs/prm/e2etests/property/NhsProperties.java +++ b/src/main/java/uk/nhs/prm/e2etests/property/NhsProperties.java @@ -1,6 +1,5 @@ package uk.nhs.prm.e2etests.property; -import com.amazonaws.services.dynamodbv2.xspec.SS; import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/uk/nhs/prm/e2etests/queue/AbstractMessageQueue.java b/src/main/java/uk/nhs/prm/e2etests/queue/AbstractMessageQueue.java index 16a8ff36..297d0942 100644 --- a/src/main/java/uk/nhs/prm/e2etests/queue/AbstractMessageQueue.java +++ b/src/main/java/uk/nhs/prm/e2etests/queue/AbstractMessageQueue.java @@ -1,5 +1,7 @@ package uk.nhs.prm.e2etests.queue; +import lombok.extern.log4j.Log4j2; +import org.awaitility.core.ConditionTimeoutException; import software.amazon.awssdk.services.sqs.model.PurgeQueueInProgressException; import uk.nhs.prm.e2etests.model.nems.NemsResolutionMessage; import org.awaitility.core.ConditionTimeoutException; @@ -26,6 +28,8 @@ @Log4j2 public abstract class AbstractMessageQueue { private static final String CHECKING_QUEUE_LOG_MESSAGE = "Checking if message is present on: {}"; + + private static final String CHECKING_QUEUE_LOG_MESSAGE_WITH_SUBSTRING = CHECKING_QUEUE_LOG_MESSAGE + ", with substring {}"; private static final String DELETE_ALL_MESSAGES_LOG_MESSAGE = "Attempting to delete all messages on: {}"; private static final String MESSAGE_FOUND_LOG_MESSAGE = "The message has been found on: {}"; @@ -53,7 +57,7 @@ public void deleteMessage(SqsMessage sqsMessage) { } public SqsMessage getMessageContaining(String substring) { - log.info(CHECKING_QUEUE_LOG_MESSAGE, this.queueUri); + log.info(CHECKING_QUEUE_LOG_MESSAGE_WITH_SUBSTRING, this.queueUri, substring); final SqsMessage foundMessage = await().atMost(120, TimeUnit.SECONDS) .with() .pollInterval(100, TimeUnit.MILLISECONDS) @@ -78,6 +82,31 @@ public List getAllMessagesContaining(String substring, int expectedN return allMessages; } + + + + public boolean verifyNoMessageContaining(String substring, int secondsToPoll) { + // Queue the queue repeatedly for {secondsToPoll}, and return true only if a message with given substring NEVER appeared throughout the period. + // The reason of using awaitility is to allow for the time taken for communication between micro-services (ehr-out, ehr-repo, gp2gp messenger) + log.info("Verifying that no message on queue {} contains the substring {}", this.queueUri, substring); + try { + await().atMost(secondsToPoll, TimeUnit.SECONDS) + .with() + .pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> findMessageContaining(substring), Optional::isPresent); + log.info("A message on {} match the given substring {}. Returning false", this.queueUri, substring); + return false; + } catch (ConditionTimeoutException error) { + log.info("Confirmed no message on {} match the given substring {}. Returning true", this.queueUri, substring); + return true; + } + } + + public boolean verifyNoMessageContaining(String substring) { + // calls the method with the same name, with secondsToPoll preset to 10 seconds. + return verifyNoMessageContaining(substring, 10); + } + public SqsMessage getMessageContainingAttribute(String attribute, String expectedValue) { return getMessageContainingAttribute(attribute, expectedValue, 120, TimeUnit.SECONDS); } diff --git a/src/main/java/uk/nhs/prm/e2etests/service/HealthCheckService.java b/src/main/java/uk/nhs/prm/e2etests/service/HealthCheckService.java new file mode 100644 index 00000000..2f6d397c --- /dev/null +++ b/src/main/java/uk/nhs/prm/e2etests/service/HealthCheckService.java @@ -0,0 +1,64 @@ +package uk.nhs.prm.e2etests.service; + +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; +import uk.nhs.prm.e2etests.exception.ServiceException; +import uk.nhs.prm.e2etests.property.EhrOutServiceProperties; +import uk.nhs.prm.e2etests.property.EhrRepositoryProperties; +import uk.nhs.prm.e2etests.property.Gp2gpMessengerProperties; + +import java.util.Map; + +@Log4j2 +@Service +public class HealthCheckService { + private final RestTemplate restTemplate = new RestTemplate(); + + private final Map listOfServices; + + @Autowired + public HealthCheckService( + Gp2gpMessengerProperties gp2gpMessengerProperties, + EhrRepositoryProperties ehrRepositoryProperties, + EhrOutServiceProperties ehrOutServiceProperties + ) { + // Ehr Transfer Service doesn't seem to have a health check endpoint for now, so it is not included here. + this.listOfServices = Map.of( + "Gp2gp Messenger", gp2gpMessengerProperties.getGp2gpMessengerUrl(), + "Ehr Repository", ehrRepositoryProperties.getEhrRepositoryUrl(), + "Ehr Out Service", ehrOutServiceProperties.getEhrOutServiceUrl() + ); + } + + public boolean healthCheckAllPassing() { + return this.listOfServices.entrySet().stream().allMatch( + entry -> runHealthCheck(entry.getKey(), entry.getValue()) + ); + } + + private boolean runHealthCheck(String nameOfService, String baseUrl) { + log.info("Running health check for service: {}", nameOfService); + String ehrRepoHealthCheckUrl = String.format("%s/health", baseUrl); + try { + ResponseEntity exchange = restTemplate.exchange(ehrRepoHealthCheckUrl, + HttpMethod.GET, new HttpEntity(createHeaders()), String.class); + return exchange.getStatusCode().is2xxSuccessful(); + } catch (HttpStatusCodeException exception) { + throw new ServiceException(getClass().getName(), exception.getMessage()); + } + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } +} diff --git a/src/main/java/uk/nhs/prm/e2etests/service/TemplatingService.java b/src/main/java/uk/nhs/prm/e2etests/service/TemplatingService.java index 94cbc8de..1eb780c2 100644 --- a/src/main/java/uk/nhs/prm/e2etests/service/TemplatingService.java +++ b/src/main/java/uk/nhs/prm/e2etests/service/TemplatingService.java @@ -2,7 +2,6 @@ import uk.nhs.prm.e2etests.model.templatecontext.NemsEventTemplateContext; import uk.nhs.prm.e2etests.model.templatecontext.TemplateContext; -import uk.nhs.prm.e2etests.utility.NhsIdentityUtility; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import uk.nhs.prm.e2etests.enumeration.TemplateVariant; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9097f961..cb29bc31 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -42,6 +42,7 @@ aws: pdsAdaptor: https://pds-adaptor.${NHS_ENVIRONMENT}.non-prod.patient-deductions.nhs.uk/ gp2GpMessenger: https://gp2gp-messenger.${NHS_ENVIRONMENT}.non-prod.patient-deductions.nhs.uk/ ehrRepository: https://ehr-repo.${NHS_ENVIRONMENT}.non-prod.patient-deductions.nhs.uk/ + ehrOutService: https://ehr-out-service.${NHS_ENVIRONMENT}.non-prod.patient-deductions.nhs.uk/ queueNames: meshForwarder: diff --git a/src/test/java/uk/nhs/prm/e2etests/test/RepositoryE2ETest.java b/src/test/java/uk/nhs/prm/e2etests/test/RepositoryE2ETest.java index f11eb1be..d49290a3 100644 --- a/src/test/java/uk/nhs/prm/e2etests/test/RepositoryE2ETest.java +++ b/src/test/java/uk/nhs/prm/e2etests/test/RepositoryE2ETest.java @@ -3,6 +3,8 @@ import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; @@ -41,6 +43,7 @@ import uk.nhs.prm.e2etests.queue.ehrtransfer.observability.EhrTransferServiceTransferCompleteOQ; import uk.nhs.prm.e2etests.queue.ehrtransfer.observability.EhrTransferServiceUnhandledOQ; import uk.nhs.prm.e2etests.queue.gp2gpmessenger.observability.Gp2GpMessengerOQ; +import uk.nhs.prm.e2etests.service.HealthCheckService; import uk.nhs.prm.e2etests.service.PdsAdaptorService; import uk.nhs.prm.e2etests.service.TemplatingService; import uk.nhs.prm.e2etests.service.TransferTrackerService; @@ -78,6 +81,7 @@ class RepositoryE2ETest { private final TransferTrackerService transferTrackerService; private final PdsAdaptorService pdsAdaptorService; private final TemplatingService templatingService; + private final HealthCheckService healthCheckService; private final SimpleAmqpQueue mhsInboundQueue; private final Gp2GpMessengerOQ gp2gpMessengerOQ; private final EhrTransferServiceTransferCompleteOQ ehrTransferServiceTransferCompleteOQ; @@ -98,6 +102,7 @@ public RepositoryE2ETest( TransferTrackerService transferTrackerService, PdsAdaptorService pdsAdaptorService, TemplatingService templatingService, + HealthCheckService healthCheckService, SimpleAmqpQueue mhsInboundQueue, Gp2GpMessengerOQ gp2gpMessengerOQ, EhrTransferServiceTransferCompleteOQ ehrTransferServiceTransferCompleteOQ, @@ -115,6 +120,7 @@ public RepositoryE2ETest( this.transferTrackerService = transferTrackerService; this.pdsAdaptorService = pdsAdaptorService; this.templatingService = templatingService; + this.healthCheckService = healthCheckService; this.mhsInboundQueue = mhsInboundQueue; this.gp2gpMessengerOQ = gp2gpMessengerOQ; this.ehrTransferServiceTransferCompleteOQ = ehrTransferServiceTransferCompleteOQ; @@ -335,6 +341,87 @@ void shouldVerifyThatALargeEhrXMLIsUnchanged() { }); } + // Test Cases for Erroneous Inbound messages + private Arguments erroneousInboundMessage_UnrecognisedInteractionID() { + String invalidInteractionId = "TEST_XX123456XX01"; + String nhsNumber = Patient.PATIENT_WITH_SMALL_EHR_IN_REPO_AND_MOF_SET_TO_TPP.nhsNumber(); + String newGpOdsCode = Gp2GpSystem.TPP_PTL_INT.odsCode(); + + EhrRequestTemplateContext ehrRequestContext = EhrRequestTemplateContext.builder() + .nhsNumber(nhsNumber) + .newGpOdsCode(newGpOdsCode) + .build(); + + String inboundMessage = this.templatingService.getTemplatedString(TemplateVariant.EHR_REQUEST, ehrRequestContext); + + String erroneousInboundMessage = inboundMessage + .replaceAll(MessageType.EHR_REQUEST.interactionId, invalidInteractionId); + + return Arguments.of( + Named.of("Message with unrecognised Interaction ID", erroneousInboundMessage), + ehrRequestContext.getOutboundConversationId() + ); + } + + private Arguments erroneousInboundMessage_EhrRequestWithUnrecognisedNhsNumber() { + String nonExistentNhsNumber = "9729999999"; + String newGpOdsCode = Gp2GpSystem.TPP_PTL_INT.odsCode(); + + EhrRequestTemplateContext ehrRequestContext = EhrRequestTemplateContext.builder() + .nhsNumber(nonExistentNhsNumber) + .newGpOdsCode(newGpOdsCode) + .build(); + + String erroneousInboundMessage = this.templatingService.getTemplatedString(TemplateVariant.EHR_REQUEST, ehrRequestContext); + + return Arguments.of( + Named.of("EHR Request with unrecognised NHS Number", erroneousInboundMessage), + ehrRequestContext.getOutboundConversationId() + ); + } + + private Arguments erroneousInboundMessage_ContinueRequestWithUnrecognisedConversationId() { + // The builder by default already generates a random conversation ID, which fulfills the test condition + ContinueRequestTemplateContext continueRequestContext = ContinueRequestTemplateContext.builder().build(); + + String continueRequestMessage = this.templatingService + .getTemplatedString(TemplateVariant.CONTINUE_REQUEST, continueRequestContext); + + return Arguments.of( + Named.of("Continue Request with unrecognised Conversation ID", continueRequestMessage), + continueRequestContext.getOutboundConversationId() + ); + } + + private Stream erroneousInboundMessages() { + return Stream.of( + erroneousInboundMessage_UnrecognisedInteractionID(), + erroneousInboundMessage_EhrRequestWithUnrecognisedNhsNumber(), + erroneousInboundMessage_ContinueRequestWithUnrecognisedConversationId() + ); + } + + @ParameterizedTest(name = "[{index}] Case of {0}") + @MethodSource("erroneousInboundMessages") + @DisplayName("Test how ORC handles Erroneous inbound messages") + void testsWithErroneousInboundMessages(String inboundMessage, String conversationId) { + // when + mhsInboundQueue.sendMessage(inboundMessage, conversationId.toLowerCase()); + + // then + log.info("Verify that EHR Transfer Service put the erroneous inbound message to unhandled queue"); + SqsMessage unhandledMessage = ehrTransferServiceUnhandledOQ.getMessageContaining(conversationId); + assertThat(unhandledMessage.getBody()).isEqualTo(inboundMessage); + + log.info("Verify that no response message with given conversation id is on the gp2gp observability queue"); + // later this could be changed to asserting an NACK message on the queue if we do send back NACKs + assertTrue(gp2gpMessengerOQ.verifyNoMessageContaining(conversationId)); + + assertTrue(healthCheckService.healthCheckAllPassing()); + } + + // End of tests for Erroneous Inbound messages + @Test @Disabled("This test was failing before refactoring. The cause seems to be related to EMIS instance not working") void shouldReceivingAndTrackAllLargeEhrFragments_DevAndTest() {