Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@
import uk.nhs.adaptors.gp2gp.common.service.XPathService;

public class ResourceHelper {

public static String loadClasspathResourceAsString(String path) {
return new Scanner(ResourceHelper.class.getResourceAsStream(path), StandardCharsets.UTF_8).useDelimiter("\\A").next();
if (path == null || path.isBlank()) {
throw new IllegalArgumentException("Classpath resource path must be provided");
}

var resourceStream = ResourceHelper.class.getResourceAsStream(path);
if (resourceStream == null) {
throw new IllegalArgumentException("Classpath resource not found: " + path);
}

try (var scanner = new Scanner(resourceStream, StandardCharsets.UTF_8).useDelimiter("\\A")) {
return scanner.hasNext() ? scanner.next() : "";
}
}

@SneakyThrows
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package uk.nhs.adaptors.gp2gp;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.junit.jupiter.api.Test;

class ResourceHelperTest {
@Test
void When_ValidClasspathResourceProvided_Expect_ResourceLoadedAsString() {
var actual = ResourceHelper.loadClasspathResourceAsString("/ehr/request/RCMR_IN010000UK05_header.xml");

assertThat(actual)
.contains("<soap:Envelope")
.contains("<eb:ConversationId>DFF5321C-C6EA-468E-BBC2-B0E48000E071</eb:ConversationId>");
}

@Test
void When_ValidXmlClasspathResourceProvided_Expect_ResourceParsedAsXmlDocument() {
var actual = ResourceHelper.loadClasspathResourceAsXml("/ehr/request/RCMR_IN010000UK05_header.xml");

assertThat(actual.getDocumentElement().getNodeName()).isEqualTo("soap:Envelope");
}

@Test
void When_ResourcePathIsBlank_Expect_ClearException() {
assertThatThrownBy(() -> ResourceHelper.loadClasspathResourceAsString(" "))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Classpath resource path must be provided");
}

@Test
void When_ResourceDoesNotExist_Expect_ClearException() {
assertThatThrownBy(() -> ResourceHelper.loadClasspathResourceAsString("/does/not/exist.xml"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Classpath resource not found: /does/not/exist.xml");
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -42,79 +43,110 @@ class TaskConsumerTest {
@Test
@SneakyThrows
void When_TaskHandlerReturnsTrue_Expect_MessageAcknowledged() {
when(taskHandler.handle(any())).thenReturn(true);
stubHandleResult(true);

taskConsumer.receive(message, session);
callReceive();

verify(taskHandler).handle(message);
verify(message).acknowledge();
verifyTaskHandled();
verify(message, times(1)).acknowledge();
verifyMdcReset();
}

@Test
@SneakyThrows
void When_TaskHandlerReturnsFalse_Expect_MessageNotAcknowledged() {
when(taskHandler.handle(any())).thenReturn(false);
stubHandleResult(false);

taskConsumer.receive(message, session);
callReceive();

verify(taskHandler).handle(message);
verify(message, times(0)).acknowledge();
verifyTaskHandled();
verify(message, never()).acknowledge();
verifyMdcReset();
}

@Test
@SneakyThrows
void When_TaskHandlerReturnsFalse_Expect_SessionRolledBack() {
when(taskHandler.handle(any())).thenReturn(false);
stubHandleResult(false);

taskConsumer.receive(message, session);
callReceive();

verify(taskHandler).handle(message);
verifyTaskHandled();
verify(session, times(1)).rollback();
verifyMdcReset();
}

@Test
@SneakyThrows
void When_TaskHandlerThrowsException_Expect_MessageNotAcknowledged() {
doThrow(RuntimeException.class).when(taskHandler).handle(message);
stubHandleThrows(RuntimeException.class);

taskConsumer.receive(message, session);
callReceive();

verify(taskHandler).handle(message);
verify(message, times(0)).acknowledge();
verifyTaskHandled();
verify(message, never()).acknowledge();
verifyMdcReset();
}

@Test
@SneakyThrows
void When_TaskHandlerThrowsException_Expect_SessionRolledBack() {
doThrow(RuntimeException.class).when(taskHandler).handle(message);
stubHandleThrows(RuntimeException.class);

taskConsumer.receive(message, session);
callReceive();

verify(taskHandler).handle(message);
verifyTaskHandled();
verify(session, times(1)).rollback();
verifyMdcReset();
}

@Test
@SneakyThrows
void When_TaskHandlerThrowsDataResourceAccessFailureException_Expect_ExceptionIsThrown() {
doThrow(DataAccessResourceFailureException.class).when(taskHandler).handle(message);
stubHandleThrows(DataAccessResourceFailureException.class);

assertThatExceptionOfType(DataAccessResourceFailureException.class)
.isThrownBy(() -> taskConsumer.receive(message, session));
.isThrownBy(this::callReceive);

verify(session, times(0)).rollback();
verify(message, times(0)).acknowledge();
verify(session, never()).rollback();
verify(message, never()).acknowledge();
verifyMdcReset();
}

@Test
@SneakyThrows
void When_TaskHandlerThrowsMhsConnectionException_Expect_ExceptionIsThrown() {
doThrow(MhsConnectionException.class).when(taskHandler).handle(message);
stubHandleThrows(MhsConnectionException.class);

assertThatExceptionOfType(MhsConnectionException.class)
.isThrownBy(() -> taskConsumer.receive(message, session));
.isThrownBy(this::callReceive);

verify(session, never()).rollback();
verify(message, never()).acknowledge();
verifyMdcReset();
}

@SneakyThrows
private void callReceive() {
taskConsumer.receive(message, session);
}

@SneakyThrows
private void stubHandleResult(boolean result) {
when(taskHandler.handle(any())).thenReturn(result);
}

@SneakyThrows
private void stubHandleThrows(Class<? extends RuntimeException> exceptionType) {
doThrow(exceptionType).when(taskHandler).handle(message);
}

@SneakyThrows
private void verifyTaskHandled() {
verify(taskHandler).handle(message);
}

verify(session, times(0)).rollback();
verify(message, times(0)).acknowledge();
private void verifyMdcReset() {
verify(mdcService).resetAllMdcKeys();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,16 @@

@ExtendWith(MockitoExtension.class)
class SendDocumentTaskExecutorTest {

private static final int SIZE_THRESHOLD_FOUR = 4;
private static final int SIZE_OF_EACH_CHUNK = 1;
private static final int NUMBER_OF_CHUNKS = 5;
private static final String STORAGE_FILE_NAME = "large_file_which_will_be_split.txt";
private static final String STORAGE_CONTENT_TYPE_UNUSED = "should-not-be-used";
private static final String RANDOM_ID = "RANDOM-ID";
private static final String RANDOM_ODS = "RANDOM-ODS";
private static final String MESSAGE_ID = "88";
private static final String TASK_CONTENT_TYPE = "should-be-used";
@Mock private EhrExtractStatusService ehrExtractStatusService;
@Mock private DetectDocumentsSentService detectDocumentsSentService;
@Mock private MhsClient mhsClient;
Expand All @@ -62,68 +71,56 @@ void When_ChunkingString_Expect_StringIsProperlySplit(String input, int sizeThre
@SneakyThrows
@Test
void When_DocumentNeedsToBeSplitIntoFiveChunks_Expect_FiveMhsRequestsWithAttachmentsOfContentTypeOctetStream() {
final int SIZE_OF_EACH_CHUNK = 1;
final int NUMBER_OF_CHUNKS = 5;
final String storageFileName = "large_file_which_will_be_split.txt";

// Arrange
this.gp2gpConfiguration.setLargeAttachmentThreshold(SIZE_OF_EACH_CHUNK);
uploadDocumentToStorageWrapperWithPayloadSize(SIZE_OF_EACH_CHUNK * NUMBER_OF_CHUNKS, storageFileName, "should-not-be-used");
uploadFiveChunkDocumentToStorage();

// Act
this.sendDocumentTaskExecutor.execute(
SendDocumentTaskDefinition.builder()
.documentName(storageFileName)
.messageId("88")
.fromOdsCode("RANDOM-ODS")
.conversationId("RANDOM-ID")
.documentContentType("should-be-used")
.build()
);
this.sendDocumentTaskExecutor.execute(createTaskDefinition());

// Assert
verify(mhsRequestBuilder, times(NUMBER_OF_CHUNKS)).buildSendEhrExtractCommonRequest(
argThat(mhsRequestBodyContainsAttachmentWithContentType(MimeTypes.OCTET_STREAM)),
eq("RANDOM-ID"),
eq("RANDOM-ODS"),
argThat(mhsRequestBodyContainsOctetStreamAttachment()),
eq(RANDOM_ID),
eq(RANDOM_ODS),
any()
);
verify(mhsClient, times(NUMBER_OF_CHUNKS + 1)).sendMessageToMHS(any());
}

@DisplayName("When_DocumentNeedsToBeSplitInto5Chunks_Expect_"
+ "MhsMessageWith5ExternalAttachmentWithDescriptionContentTypeHeaderFromTaskDefinition")
@Test
void When_DocumentNeedsToBeSplitInto5Chunks_Expect_MhsMessageWith5ExternalAttachmentCorrectlySet() {
final int SIZE_OF_EACH_CHUNK = 1;
final int NUMBER_OF_CHUNKS = 5;
final String storageFileName = "large_file_which_will_be_split.txt";

// Arrange
this.gp2gpConfiguration.setLargeAttachmentThreshold(SIZE_OF_EACH_CHUNK);
uploadDocumentToStorageWrapperWithPayloadSize(SIZE_OF_EACH_CHUNK * NUMBER_OF_CHUNKS, storageFileName, "should-not-be-used");
uploadFiveChunkDocumentToStorage();

// Act
this.sendDocumentTaskExecutor.execute(
SendDocumentTaskDefinition.builder()
.documentName(storageFileName)
.messageId("88")
.fromOdsCode("RANDOM-ODS")
.conversationId("RANDOM-ID")
.documentContentType("should-be-used")
.build()
);
this.sendDocumentTaskExecutor.execute(createTaskDefinition());

// Assert
verify(mhsRequestBuilder, times(1)).buildSendEhrExtractCommonRequest(
argThat(mhsRequestBodyWithAnExternalAttachmentForEachChunkWithContentType(NUMBER_OF_CHUNKS, "should-be-used")),
eq("RANDOM-ID"),
eq("RANDOM-ODS"),
argThat(mhsRequestBodyHasExternalAttachmentForEachChunkWithTaskContentType()),
eq(RANDOM_ID),
eq(RANDOM_ODS),
any()
);
verify(mhsClient, times(NUMBER_OF_CHUNKS + 1)).sendMessageToMHS(any());
}

private SendDocumentTaskDefinition createTaskDefinition() {
return SendDocumentTaskDefinition.builder()
.documentName(STORAGE_FILE_NAME)
.messageId(MESSAGE_ID)
.fromOdsCode(RANDOM_ODS)
.conversationId(RANDOM_ID)
.documentContentType(TASK_CONTENT_TYPE)
.build();
}

@NotNull
private static ArgumentMatcher<String> mhsRequestBodyContainsAttachmentWithContentType(String contentType) {
private static ArgumentMatcher<String> mhsRequestBodyContainsOctetStreamAttachment() {
return mhsRequestBody -> {
ObjectMapper objectMapper = new ObjectMapper();
OutboundMessage outboundMessage;
Expand All @@ -135,13 +132,13 @@ private static ArgumentMatcher<String> mhsRequestBodyContainsAttachmentWithConte
if (outboundMessage.getAttachments().isEmpty()) {
return false;
}
return Objects.equals(outboundMessage.getAttachments().get(0).getContentType(), contentType);
return Objects.equals(outboundMessage.getAttachments().getFirst().getContentType(), MimeTypes.OCTET_STREAM);
};
}

@NotNull
private static ArgumentMatcher<String>
mhsRequestBodyWithAnExternalAttachmentForEachChunkWithContentType(int numberOfChunks, String contentType) {
mhsRequestBodyHasExternalAttachmentForEachChunkWithTaskContentType() {

return mhsRequestBody -> {
ObjectMapper objectMapper = new ObjectMapper();
Expand All @@ -151,28 +148,28 @@ private static ArgumentMatcher<String> mhsRequestBodyContainsAttachmentWithConte
} catch (JsonProcessingException e) {
return false;
}
if (outboundMessage.getExternalAttachments() == null || outboundMessage.getExternalAttachments().size() != numberOfChunks) {
if (outboundMessage.getExternalAttachments() == null
|| outboundMessage.getExternalAttachments().size() != NUMBER_OF_CHUNKS) {
return false;
}
return outboundMessage.getExternalAttachments().stream().allMatch(
externalAttachment -> externalAttachment.getDescription().contains("ContentType=" + contentType)
externalAttachment -> externalAttachment.getDescription().contains("ContentType=" + TASK_CONTENT_TYPE)
);
};
}

private void uploadDocumentToStorageWrapperWithPayloadSize(int payloadSize, String storageFileName, String contentType) {
byte[] storageDataWrapper = generateStorageDataWrapper(payloadSize, contentType);
private void uploadFiveChunkDocumentToStorage() {
byte[] storageDataWrapper = generateStorageDataWrapper();
this.storageConnector.uploadToStorage(
new ByteArrayInputStream(storageDataWrapper),
storageDataWrapper.length,
storageFileName
STORAGE_FILE_NAME
);
}

@NotNull
private static byte[] generateStorageDataWrapper(int sizeOfPayload, String contentType) {
String payload = "a".repeat(sizeOfPayload);
String attachment = "{\"content_type\":\"" + contentType + "\",\"is_base64\":false"
private static byte[] generateStorageDataWrapper() {
String payload = "a".repeat(SIZE_OF_EACH_CHUNK * NUMBER_OF_CHUNKS);
String attachment = "{\"content_type\":\"" + STORAGE_CONTENT_TYPE_UNUSED + "\",\"is_base64\":false"
+ ",\"description\":\"\",\"payload\":\"" + payload + "\"}";
String outboundMessage = "{\"payload\": \"\", \"attachments\": [" + attachment + "], \"external_attachments\": []}";
String encodedData = outboundMessage.replace("\"", "\\\""); // Poor persons JSON encode
Expand Down
Loading
Loading