diff --git a/etor/databaseMigrations/message_link.yml b/etor/databaseMigrations/message_link.yml new file mode 100644 index 000000000..e3e23901d --- /dev/null +++ b/etor/databaseMigrations/message_link.yml @@ -0,0 +1,40 @@ +databaseChangeLog: + - changeSet: + id: 1 + author: basiliskus + labels: create-message_link-table + context: message_link + comment: create message_link table + changes: + - createTable: + tableName: message_link + columns: + - column: + name: id + type: int + constraints: + primaryKey: true + nullable: false + autoIncrement: true + - column: + name: link_id + type: uuid + constraints: + nullable: false + - column: + name: message_id + type: varchar(40) + constraints: + nullable: false + - addUniqueConstraint: + tableName: message_link + columnNames: link_id, message_id + constraintName: message_link_link_id_message_id_key + - addForeignKeyConstraint: + baseTableName: message_link + baseColumnNames: message_id + constraintName: metadata_received_message_id_fkey + referencedTableName: metadata + referencedColumnNames: received_message_id + onDelete: CASCADE + onUpdate: CASCADE diff --git a/etor/databaseMigrations/metadata.yml b/etor/databaseMigrations/metadata.yml index 1d14c95b0..1712dfbc7 100644 --- a/etor/databaseMigrations/metadata.yml +++ b/etor/databaseMigrations/metadata.yml @@ -84,6 +84,7 @@ databaseChangeLog: - column: name: message_type type: message_type + - changeSet: id: 5 author: halprin @@ -109,6 +110,7 @@ databaseChangeLog: - column: name: receiving_facility_id type: varchar(227) + - changeSet: id: 6 author: samuel.aquino diff --git a/etor/databaseMigrations/root.yml b/etor/databaseMigrations/root.yml index d03ecbdc1..e5317b270 100644 --- a/etor/databaseMigrations/root.yml +++ b/etor/databaseMigrations/root.yml @@ -1,6 +1,8 @@ databaseChangeLog: - include: file: etor/databaseMigrations/metadata.yml + - include: + file: etor/databaseMigrations/message_link.yml # Please put all other migrations above this comment - include: file: etor/databaseMigrations/azure.yml diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java index 274ebe74b..6113f3487 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java @@ -11,6 +11,7 @@ import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics; import gov.hhs.cdc.trustedintermediary.etor.demographics.PatientDemographicsController; import gov.hhs.cdc.trustedintermediary.etor.demographics.PatientDemographicsResponse; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkStorage; import gov.hhs.cdc.trustedintermediary.etor.messages.MessageRequestHandler; import gov.hhs.cdc.trustedintermediary.etor.messages.SendMessageHelper; import gov.hhs.cdc.trustedintermediary.etor.messages.UnableToSendMessageException; @@ -34,6 +35,7 @@ import gov.hhs.cdc.trustedintermediary.etor.results.SendResultUseCase; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleEngine; import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleLoader; +import gov.hhs.cdc.trustedintermediary.external.database.DatabaseMessageLinkStorage; import gov.hhs.cdc.trustedintermediary.external.database.DatabasePartnerMetadataStorage; import gov.hhs.cdc.trustedintermediary.external.database.DbDao; import gov.hhs.cdc.trustedintermediary.external.database.PostgresDao; @@ -42,6 +44,7 @@ import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrderConverter; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiPartnerMetadataConverter; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiResultConverter; +import gov.hhs.cdc.trustedintermediary.external.localfile.FileMessageLinkStorage; import gov.hhs.cdc.trustedintermediary.external.localfile.FilePartnerMetadataStorage; import gov.hhs.cdc.trustedintermediary.external.localfile.MockRSEndpointClient; import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamEndpointClient; @@ -137,9 +140,13 @@ public Map> domainRegistra ApplicationContext.register(DbDao.class, PostgresDao.getInstance()); ApplicationContext.register( PartnerMetadataStorage.class, DatabasePartnerMetadataStorage.getInstance()); + ApplicationContext.register( + MessageLinkStorage.class, DatabaseMessageLinkStorage.getInstance()); } else if (ApplicationContext.getEnvironment().equalsIgnoreCase("local")) { ApplicationContext.register( PartnerMetadataStorage.class, FilePartnerMetadataStorage.getInstance()); + ApplicationContext.register( + MessageLinkStorage.class, FileMessageLinkStorage.getInstance()); } if (ApplicationContext.getEnvironment().equalsIgnoreCase("local")) { ApplicationContext.register(RSEndpointClient.class, MockRSEndpointClient.getInstance()); @@ -154,15 +161,19 @@ public Map> domainRegistra @Override public String openApiSpecification() throws UnableToReadOpenApiSpecificationException { String fileName = "openapi_etor.yaml"; - try (InputStream openApiStream = - getClass().getClassLoader().getResourceAsStream(fileName)) { - return new String(openApiStream.readAllBytes(), StandardCharsets.UTF_8); + try { + return openApiStream(fileName); } catch (IOException e) { throw new UnableToReadOpenApiSpecificationException( "Failed to open OpenAPI specification for " + fileName, e); } } + public String openApiStream(String fileName) throws IOException { + InputStream openApiStream = getClass().getClassLoader().getResourceAsStream(fileName); + return new String(openApiStream.readAllBytes(), StandardCharsets.UTF_8); + } + DomainResponse handleDemographics(DomainRequest request) { Demographics demographics; diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLink.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLink.java new file mode 100644 index 000000000..b712f64ac --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLink.java @@ -0,0 +1,52 @@ +package gov.hhs.cdc.trustedintermediary.etor.messagelink; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * This class represents a link between messages. Each link has a unique ID and is associated with a + * set of message IDs to link. + */ +public final class MessageLink { + private UUID linkId; + private Set messageIds; + + public MessageLink() { + this.messageIds = new HashSet<>(); + } + + public MessageLink(UUID linkId, String messageId) { + this.linkId = linkId; + this.messageIds = Set.of(messageId); + } + + public MessageLink(UUID linkId, Set messageIds) { + this.linkId = linkId; + this.messageIds = new HashSet<>(messageIds); + } + + public void setLinkId(UUID linkId) { + this.linkId = linkId; + } + + public UUID getLinkId() { + return linkId; + } + + public void setMessageIds(Set messageIds) { + this.messageIds = messageIds; + } + + public Set getMessageIds() { + return this.messageIds; + } + + public void addMessageId(String messageId) { + this.messageIds.add(messageId); + } + + public void addMessageIds(Set messageIds) { + this.messageIds.addAll(messageIds); + } +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkException.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkException.java new file mode 100644 index 000000000..46058cca5 --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkException.java @@ -0,0 +1,9 @@ +package gov.hhs.cdc.trustedintermediary.etor.messagelink; + +/** This exception is thrown when there is an error linking messages. */ +public class MessageLinkException extends Exception { + + public MessageLinkException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkStorage.java new file mode 100644 index 000000000..32b1ef369 --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkStorage.java @@ -0,0 +1,10 @@ +package gov.hhs.cdc.trustedintermediary.etor.messagelink; + +import java.util.Optional; + +/** This interface defines the methods for storing and retrieving message links. */ +public interface MessageLinkStorage { + Optional getMessageLink(String messageId) throws MessageLinkException; + + void saveMessageLink(MessageLink messageLink) throws MessageLinkException; +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelper.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelper.java index 66e381e66..45e307f39 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelper.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelper.java @@ -1,9 +1,11 @@ package gov.hhs.cdc.trustedintermediary.etor.messages; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkException; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataException; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataMessageType; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataOrchestrator; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; +import java.util.Set; import javax.inject.Inject; public class SendMessageHelper { @@ -70,4 +72,28 @@ public void saveSentMessageSubmissionId(String receivedSubmissionId, String sent e); } } + + public void linkMessage(String receivedSubmissionId) { + if (receivedSubmissionId == null) { + logger.logWarning("Received submissionId is null so not linking messages"); + return; + } + + try { + Set messageIdsToLink = + partnerMetadataOrchestrator.findMessagesIdsToLink(receivedSubmissionId); + + if (messageIdsToLink == null || messageIdsToLink.isEmpty()) { + return; + } + logger.logInfo( + "Found messages to link for receivedSubmissionId {}: {}", + receivedSubmissionId, + messageIdsToLink); + partnerMetadataOrchestrator.linkMessages(messageIdsToLink); + } catch (PartnerMetadataException | MessageLinkException e) { + logger.logError( + "Unable to link messages for received submissionId " + receivedSubmissionId, e); + } + } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestrator.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestrator.java index ab12da5a8..bc57a2752 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestrator.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestrator.java @@ -1,6 +1,9 @@ package gov.hhs.cdc.trustedintermediary.etor.metadata.partner; import gov.hhs.cdc.trustedintermediary.etor.RSEndpointClient; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLink; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkException; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkStorage; import gov.hhs.cdc.trustedintermediary.etor.messages.MessageHdDataType; import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamEndpointClientException; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; @@ -13,6 +16,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import javax.inject.Inject; @@ -26,6 +31,7 @@ public class PartnerMetadataOrchestrator { private static final PartnerMetadataOrchestrator INSTANCE = new PartnerMetadataOrchestrator(); @Inject PartnerMetadataStorage partnerMetadataStorage; + @Inject MessageLinkStorage messageLinkStorage; @Inject RSEndpointClient rsclient; @Inject Formatter formatter; @Inject Logger logger; @@ -252,6 +258,39 @@ public Map> getConsolidatedMetadata(String senderNam })); } + public Set findMessagesIdsToLink(String receivedSubmissionId) + throws PartnerMetadataException { + var metadataSet = + partnerMetadataStorage.readMetadataForMessageLinking(receivedSubmissionId); + return metadataSet.stream() + .map(PartnerMetadata::receivedSubmissionId) + .collect(Collectors.toSet()); + } + + public void linkMessages(Set messageIds) throws MessageLinkException { + Optional existingMessageLink = Optional.empty(); + for (String messageId : messageIds) { + existingMessageLink = messageLinkStorage.getMessageLink(messageId); + if (existingMessageLink.isPresent()) { + break; + } + } + + if (existingMessageLink.isEmpty()) { + logger.logInfo("Saving new message link for messageIds: {}", messageIds); + messageLinkStorage.saveMessageLink(new MessageLink(UUID.randomUUID(), messageIds)); + return; + } + + MessageLink messageLink = existingMessageLink.get(); + messageLink.addMessageIds(messageIds); + logger.logInfo( + "Updating existing message link {} with messageIds: {}", + messageLink.getLinkId(), + messageIds); + messageLinkStorage.saveMessageLink(messageLink); + } + String[] getDataFromReportStream(String responseBody) throws FormatterProcessingException { // the expected json structure for the response is: // { diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataStorage.java index 217b9fdba..d7fee8ec0 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataStorage.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataStorage.java @@ -30,4 +30,7 @@ Optional readMetadata(String receivedSubmissionId) * @return a set of {@link PartnerMetadata}s. */ Set readMetadataForSender(String sender) throws PartnerMetadataException; + + Set readMetadataForMessageLinking(String submissionId) + throws PartnerMetadataException; } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java index a6b3485ff..1e1b8702d 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java @@ -50,6 +50,9 @@ public void convertAndSend(final Order order, String receivedSubmissionId) EtorMetadataStep.ETOR_PROCESSING_TAG_ADDED_TO_MESSAGE_HEADER); String sentSubmissionId = sender.send(omlOrder).orElse(null); + logger.logInfo("Sent order submissionId: {}", sentSubmissionId); + + sendMessageHelper.linkMessage(receivedSubmissionId); sendMessageHelper.saveSentMessageSubmissionId(receivedSubmissionId, sentSubmissionId); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCase.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCase.java index 673ab1012..add2a4fcb 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCase.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCase.java @@ -48,6 +48,8 @@ public void convertAndSend(Result result, String receivedSubmissionId) String sentSubmissionId = sender.send(convertedResult).orElse(null); logger.logInfo("Sent result submissionId: {}", sentSubmissionId); + sendMessageHelper.linkMessage(receivedSubmissionId); + sendMessageHelper.saveSentMessageSubmissionId(receivedSubmissionId, sentSubmissionId); } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabaseMessageLinkStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabaseMessageLinkStorage.java new file mode 100644 index 000000000..523204b1d --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabaseMessageLinkStorage.java @@ -0,0 +1,93 @@ +package gov.hhs.cdc.trustedintermediary.external.database; + +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLink; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkException; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkStorage; +import gov.hhs.cdc.trustedintermediary.wrappers.Logger; +import gov.hhs.cdc.trustedintermediary.wrappers.database.ConnectionPool; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import javax.inject.Inject; + +/** Implements the {@link MessageLinkStorage} using a database. */ +public class DatabaseMessageLinkStorage implements MessageLinkStorage { + + @Inject DbDao dao; + + @Inject ConnectionPool connectionPool; + + @Inject Logger logger; + + private static final DatabaseMessageLinkStorage INSTANCE = new DatabaseMessageLinkStorage(); + + private DatabaseMessageLinkStorage() {} + + public static DatabaseMessageLinkStorage getInstance() { + return INSTANCE; + } + + @Override + public Optional getMessageLink(String messageId) throws MessageLinkException { + var sql = + """ + SELECT * + FROM message_link + WHERE message_id = ?; + """; + + try { + UUID linkId = null; + Set messageIds = new HashSet<>(); + try (Connection conn = connectionPool.getConnection(); + PreparedStatement statement = conn.prepareStatement(sql)) { + statement.setString(1, messageId); + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + if (linkId == null) { + linkId = UUID.fromString(resultSet.getString("link_id")); + } + messageIds.add(resultSet.getString("message_id")); + } + } + } + + if (!messageIds.isEmpty()) { + return Optional.of(new MessageLink(linkId, messageIds)); + } else { + return Optional.empty(); + } + } catch (SQLException e) { + throw new MessageLinkException("Error retrieving message links", e); + } + } + + @Override + public void saveMessageLink(MessageLink messageLink) throws MessageLinkException { + logger.logInfo("Saving message links"); + try { + UUID linkId = messageLink.getLinkId(); + List columns; + for (String messageId : messageLink.getMessageIds()) { + columns = + List.of( + new DbColumn("link_id", linkId, false, Types.VARCHAR), + new DbColumn("message_id", messageId, false, Types.VARCHAR)); + dao.upsertData( + "message_link", + columns, + "ON CONSTRAINT message_link_link_id_message_id_key"); + } + } catch (SQLException e) { + throw new MessageLinkException("Error saving message links", e); + } + } +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorage.java index abd61f6e6..35ac1a61f 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorage.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorage.java @@ -50,7 +50,7 @@ public void saveMetadata(final PartnerMetadata metadata) throws PartnerMetadataE try { List columns = createDbColumnsFromMetadata(metadata); - dao.upsertData("metadata", columns, "received_message_id"); + dao.upsertData("metadata", columns, "(received_message_id)"); } catch (SQLException e) { throw new PartnerMetadataException("Error saving metadata", e); } catch (FormatterProcessingException e) { @@ -72,6 +72,18 @@ public Set readMetadataForSender(String sender) return consolidatedMetadata; } + @Override + public Set readMetadataForMessageLinking(String submissionId) + throws PartnerMetadataException { + Set metadataSet; + try { + metadataSet = dao.fetchMetadataForMessageLinking(submissionId); + } catch (SQLException | FormatterProcessingException e) { + throw new PartnerMetadataException("Error retrieving metadata", e); + } + return metadataSet; + } + private List createDbColumnsFromMetadata(PartnerMetadata metadata) throws FormatterProcessingException { return List.of( diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DbDao.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DbDao.java index 1b0420372..074566731 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DbDao.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DbDao.java @@ -8,11 +8,14 @@ /** Interface for accessing the database for metadata */ public interface DbDao { - void upsertData(String tableName, List values, String conflictColumnName) + void upsertData(String tableName, List values, String conflictTarget) throws SQLException; + Object fetchMetadata(String uniqueId) throws SQLException, FormatterProcessingException; + Set fetchMetadataForSender(String sender) throws SQLException, FormatterProcessingException; - Object fetchMetadata(String uniqueId) throws SQLException, FormatterProcessingException; + Set fetchMetadataForMessageLinking(String submissionId) + throws SQLException, FormatterProcessingException; } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java index 1656188bc..91337383a 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java @@ -34,7 +34,7 @@ public static PostgresDao getInstance() { } @Override - public void upsertData(String tableName, List values, String conflictColumnName) + public void upsertData(String tableName, List values, String conflictTarget) throws SQLException { // example SQL statement generated here: // INSERT INTO metadata_table (column_one, column_three, column_two, column_four) @@ -54,25 +54,27 @@ public void upsertData(String tableName, List values, String conflictC removeLastTwoCharacters(sqlStatementBuilder); // remove the last unused ", " sqlStatementBuilder.append(")"); - boolean wantsUpsert = values.stream().anyMatch(DbColumn::upsertOverwrite); + if (conflictTarget != null) { + sqlStatementBuilder.append(" ON CONFLICT ").append(conflictTarget); - if (wantsUpsert) { - sqlStatementBuilder - .append(" ON CONFLICT (") - .append(conflictColumnName) - .append(") DO UPDATE SET "); + boolean overwriteOnConflict = values.stream().anyMatch(DbColumn::upsertOverwrite); + if (overwriteOnConflict) { + sqlStatementBuilder.append(" DO UPDATE SET "); - for (DbColumn column : values) { - if (!column.upsertOverwrite()) { - continue; + for (DbColumn column : values) { + if (!column.upsertOverwrite()) { + continue; + } + + sqlStatementBuilder.append(column.name()).append(" = EXCLUDED."); + sqlStatementBuilder.append(column.name()); + sqlStatementBuilder.append(", "); } - sqlStatementBuilder.append(column.name()).append(" = EXCLUDED."); - sqlStatementBuilder.append(column.name()); - sqlStatementBuilder.append(", "); + removeLastTwoCharacters(sqlStatementBuilder); // remove the last unused ", " + } else { + sqlStatementBuilder.append(" DO NOTHING"); } - - removeLastTwoCharacters(sqlStatementBuilder); // remove the last unused ", " } String sqlStatement = sqlStatementBuilder.toString(); @@ -137,6 +139,35 @@ public PartnerMetadata fetchMetadata(String submissionId) } } + @Override + public Set fetchMetadataForMessageLinking(String submissionId) + throws SQLException, FormatterProcessingException { + var sql = + """ + SELECT m2.* + FROM metadata m1 + JOIN metadata m2 + ON m1.placer_order_number = m2.placer_order_number + AND m1.sending_application_id = m2.sending_application_id + AND m1.sending_facility_id = m2.sending_facility_id + WHERE m1.sent_message_id = ?; + """; + + try (Connection conn = connectionPool.getConnection(); + PreparedStatement statement = conn.prepareStatement(sql)) { + statement.setString(1, submissionId); + ResultSet resultSet = statement.executeQuery(); + + Set metadataSet = new HashSet<>(); + + while (resultSet.next()) { + metadataSet.add(partnerMetadataFromResultSet(resultSet)); + } + + return metadataSet; + } + } + private void removeLastTwoCharacters(StringBuilder stringBuilder) { stringBuilder.delete(stringBuilder.length() - 2, stringBuilder.length()); } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FileMessageLinkStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FileMessageLinkStorage.java new file mode 100644 index 000000000..8b6cb8dfc --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FileMessageLinkStorage.java @@ -0,0 +1,107 @@ +package gov.hhs.cdc.trustedintermediary.external.localfile; + +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLink; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkException; +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkStorage; +import gov.hhs.cdc.trustedintermediary.wrappers.Logger; +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter; +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException; +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import javax.inject.Inject; + +/** Implements the {@link MessageLinkStorage} using local files. */ +public class FileMessageLinkStorage implements MessageLinkStorage { + + private static final FileMessageLinkStorage INSTANCE = new FileMessageLinkStorage(); + + @Inject Formatter formatter; + @Inject Logger logger; + + static final Path MESSAGE_LINK_FILE_PATH; + + static { + try { + Path userTempPath = Paths.get(System.getProperty("java.io.tmpdir")); + MESSAGE_LINK_FILE_PATH = userTempPath.resolve("cdctimessagelink.json"); + Files.writeString( + MESSAGE_LINK_FILE_PATH, + "[]", + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private FileMessageLinkStorage() {} + + public static FileMessageLinkStorage getInstance() { + return INSTANCE; + } + + @Override + public synchronized Optional getMessageLink(String messageId) + throws MessageLinkException { + try { + Set messageLinks = readMessageLinks(); + List foundMessageLinks = + messageLinks.stream() + .filter(link -> link.getMessageIds().contains(messageId)) + .toList(); + + if (foundMessageLinks.size() > 1) { + logger.logWarning("More than one message link found for messageId: {}", messageId); + } + + return foundMessageLinks.stream().findFirst(); + } catch (IOException | FormatterProcessingException e) { + throw new MessageLinkException("Error retrieving message links", e); + } + } + + @Override + public synchronized void saveMessageLink(MessageLink messageLink) throws MessageLinkException { + try { + Set messageLinks = readMessageLinks(); + Optional existingLink = + messageLinks.stream() + .filter( + link -> + Objects.equals( + link.getLinkId(), messageLink.getLinkId())) + .findFirst(); + if (existingLink.isPresent()) { + MessageLink existing = existingLink.get(); + existing.addMessageIds(messageLink.getMessageIds()); + } else { + messageLinks.add(messageLink); + } + writeMessageLinks(messageLinks); + } catch (IOException | FormatterProcessingException e) { + throw new MessageLinkException("Error saving message links", e); + } + } + + private Set readMessageLinks() throws IOException, FormatterProcessingException { + String messageLinkContent = Files.readString(MESSAGE_LINK_FILE_PATH); + Set messageLinks = + formatter.convertJsonToObject(messageLinkContent, new TypeReference<>() {}); + return messageLinks != null ? messageLinks : new HashSet<>(); + } + + private void writeMessageLinks(Set messageLinks) + throws IOException, FormatterProcessingException { + String json = formatter.convertToJsonString(messageLinks); + Files.writeString(MESSAGE_LINK_FILE_PATH, json, StandardOpenOption.TRUNCATE_EXISTING); + } +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java index 6546c28ec..1d9ffddef 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java @@ -99,35 +99,73 @@ public void saveMetadata(final PartnerMetadata metadata) throws PartnerMetadataE @Override public Set readMetadataForSender(String sender) throws PartnerMetadataException { + try { + return getPartnerMetadata().stream() + .filter(metadata -> metadata.sender().equals(sender)) + .collect(Collectors.toSet()); + } catch (Exception e) { + throw new PartnerMetadataException("Failed reading metadata for sender: " + sender, e); + } + } - Set partnerMetadata = null; + @Override + public Set readMetadataForMessageLinking(String receivedSubmissionId) + throws PartnerMetadataException { + try { + Set existingMetadata = getPartnerMetadata(); + PartnerMetadata match = + existingMetadata.stream() + .filter( + metadata -> + metadata.receivedSubmissionId() + .equals(receivedSubmissionId)) + .findFirst() + .orElse(null); - try (Stream fileList = Files.list(METADATA_DIRECTORY)) { - partnerMetadata = - fileList.map( - fileName -> { - try { - return Files.readString(fileName); - } catch (IOException e) { - throw new RuntimeException(e); - } - }) - .map( - metadataContent -> { - try { - return formatter.convertJsonToObject( - metadataContent, - new TypeReference() {}); - } catch (FormatterProcessingException e) { - throw new RuntimeException(e); - } - }) - .filter(metadata -> metadata.sender().equals(sender)) - .collect(Collectors.toSet()); + if (match == null) { + logger.logWarning( + "Matching metadata not found for receivedSubmissionId: {}", + receivedSubmissionId); + return Set.of(); + } + + return existingMetadata.stream() + .filter( + metadata -> + metadata.placerOrderNumber().equals(match.placerOrderNumber()) + && metadata.sendingApplicationDetails() + .equals(match.sendingApplicationDetails()) + && metadata.sendingFacilityDetails() + .equals(match.sendingFacilityDetails())) + .collect(Collectors.toSet()); } catch (Exception e) { - throw new PartnerMetadataException("Failed reading metadata for sender: " + sender, e); + throw new PartnerMetadataException( + "Failed reading metadata for submissionId: " + receivedSubmissionId, e); + } + } + + private Set getPartnerMetadata() throws IOException { + try (Stream fileList = Files.list(METADATA_DIRECTORY)) { + return fileList.map( + fileName -> { + try { + return Files.readString(fileName); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .map( + metadataContent -> { + try { + return formatter.convertJsonToObject( + metadataContent, + new TypeReference() {}); + } catch (FormatterProcessingException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toSet()); } - return partnerMetadata; } private Path getFilePath(String metadataId) { diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy index 10309131d..ccb2d7086 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistrationTest.groovy @@ -8,6 +8,7 @@ import gov.hhs.cdc.trustedintermediary.domainconnector.DomainRequest import gov.hhs.cdc.trustedintermediary.domainconnector.DomainResponse import gov.hhs.cdc.trustedintermediary.domainconnector.DomainResponseHelper import gov.hhs.cdc.trustedintermediary.domainconnector.HttpEndpoint +import gov.hhs.cdc.trustedintermediary.domainconnector.UnableToReadOpenApiSpecificationException import gov.hhs.cdc.trustedintermediary.etor.demographics.ConvertAndSendDemographicsUsecase import gov.hhs.cdc.trustedintermediary.etor.demographics.Demographics import gov.hhs.cdc.trustedintermediary.etor.demographics.PatientDemographicsController @@ -31,11 +32,17 @@ import gov.hhs.cdc.trustedintermediary.etor.results.SendResultUseCase import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir import gov.hhs.cdc.trustedintermediary.wrappers.Logger + import java.time.Instant import spock.lang.Specification class EtorDomainRegistrationTest extends Specification { + private sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") + private sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") + private receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") + private receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") + def setup() { TestApplicationContext.reset() TestApplicationContext.init() @@ -48,11 +55,27 @@ class EtorDomainRegistrationTest extends Specification { def demographicsEndpoint = new HttpEndpoint("POST", EtorDomainRegistration.DEMOGRAPHICS_API_ENDPOINT, true) def ordersEndpoint = new HttpEndpoint("POST", EtorDomainRegistration.ORDERS_API_ENDPOINT, true) def metadataEndpoint = new HttpEndpoint("GET", EtorDomainRegistration.METADATA_API_ENDPOINT, true) - def consolidatedOrdersEndpoint = new HttpEndpoint("GET", EtorDomainRegistration.CONSOLIDATED_SUMMARY_API_ENDPOINT, true) - def resultsEndpoint = new HttpEndpoint("POST", EtorDomainRegistration.RESULTS_API_ENDPOINT, true) + when: + def endpoints = domainRegistration.domainRegistration() + then: + !endpoints.isEmpty() + endpoints.get(demographicsEndpoint) != null + endpoints.get(ordersEndpoint) != null + endpoints.get(metadataEndpoint) != null + endpoints.get(consolidatedOrdersEndpoint) != null + } + + def "domain registration has endpoints when DB_URL is not found"() { + given: + def domainRegistration = new EtorDomainRegistration() + def demographicsEndpoint = new HttpEndpoint("POST", EtorDomainRegistration.DEMOGRAPHICS_API_ENDPOINT, true) + def ordersEndpoint = new HttpEndpoint("POST", EtorDomainRegistration.ORDERS_API_ENDPOINT, true) + def metadataEndpoint = new HttpEndpoint("GET", EtorDomainRegistration.METADATA_API_ENDPOINT, true) + def consolidatedOrdersEndpoint = new HttpEndpoint("GET", EtorDomainRegistration.CONSOLIDATED_SUMMARY_API_ENDPOINT, true) + TestApplicationContext.addEnvironmentVariable("DB_URL", "") when: def endpoints = domainRegistration.domainRegistration() @@ -78,6 +101,30 @@ class EtorDomainRegistrationTest extends Specification { openApiSpecification.contains("paths:") } + def "correctly handles errors when loading OpenAPI"() { + given: + def domainRegistration = Spy(EtorDomainRegistration) + domainRegistration.openApiStream(_ as String) >> { throw new IOException()} + + when: + domainRegistration.openApiSpecification() + + then: + thrown(UnableToReadOpenApiSpecificationException) + } + + def "openApiStream assertion behaves correctly with bad filenames"() { + given: + def domainRegistration = Spy(EtorDomainRegistration) + domainRegistration.openApiStream(_ as String) >> { throw new IOException()} + + when: + domainRegistration.openApiStream("badFile") + + then: + thrown(IOException) + } + def "stitches the demographics parsing to the response construction"() { given: def domainRegistration = new EtorDomainRegistration() @@ -233,13 +280,11 @@ class EtorDomainRegistrationTest extends Specification { def request = new DomainRequest() request.setPathParams(["id": "metadataId"]) - def sendingAppDetails = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacilityDetails = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingAppDetails = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacilityDetails = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - def mockPartnerMetadataOrchestrator = Mock(PartnerMetadataOrchestrator) - mockPartnerMetadataOrchestrator.getMetadata(_ as String) >> Optional.ofNullable(new PartnerMetadata("receivedSubmissionId", "sender", Instant.now(), null, "hash", PartnerMetadataStatus.DELIVERED, PartnerMetadataMessageType.ORDER, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number")) + mockPartnerMetadataOrchestrator.getMetadata(_ as String) >> Optional.ofNullable( + new PartnerMetadata("receivedSubmissionId", "sender", Instant.now(), null, + "hash", PartnerMetadataStatus.DELIVERED, PartnerMetadataMessageType.ORDER, + sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number")) TestApplicationContext.register(PartnerMetadataOrchestrator, mockPartnerMetadataOrchestrator) def mockResponseHelper = Mock(DomainResponseHelper) diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkExceptionTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkExceptionTest.groovy new file mode 100644 index 000000000..ac43dc8ab --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkExceptionTest.groovy @@ -0,0 +1,21 @@ +package gov.hhs.cdc.trustedintermediary.etor.messagelink + +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException +import spock.lang.Specification + +class MessageLinkExceptionTest extends Specification { + def "constructor works"() { + given: + def message = "exception message" + def cause = new FormatterProcessingException(message, new IOException()) + + when: + def exceptionWithoutCause = new MessageLinkException(message, new Exception()) + def exceptionWithCause = new MessageLinkException(message, cause) + + then: + exceptionWithCause.getMessage() == message + exceptionWithCause.getCause() == cause + exceptionWithoutCause.getMessage() == message + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkTest.groovy new file mode 100644 index 000000000..57dc62881 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messagelink/MessageLinkTest.groovy @@ -0,0 +1,96 @@ +package gov.hhs.cdc.trustedintermediary.etor.messagelink + +import gov.hhs.cdc.trustedintermediary.PojoTestUtils +import spock.lang.Specification + +class MessageLinkTest extends Specification { + + def "test getters and setters"() { + when: + PojoTestUtils.validateGettersAndSetters(MessageLink.class) + + then: + noExceptionThrown() + } + + def "constructor without parameters initializes with empty messageIds"() { + when: + def messageLink = new MessageLink() + + then: + messageLink.getMessageIds().isEmpty() + } + + def "constructor with linkId and single messageId initializes correctly"() { + given: + def linkId = UUID.randomUUID() + def messageId = "messageId" + + when: + def messageLink = new MessageLink(linkId, messageId) + + then: + messageLink.getLinkId() == linkId + messageLink.getMessageIds().size() == 1 + messageLink.getMessageIds().contains(messageId) + } + + def "constructor with linkId and messageId set initializes correctly"() { + given: + def linkId = UUID.randomUUID() + def messageIdSet = Set.of("messageId1", "messageId2") + + when: + def messageLink = new MessageLink(linkId, messageIdSet) + + then: + messageLink.getLinkId() == linkId + messageLink.getMessageIds() == messageIdSet + } + + def "addMessageId adds messageId to the messageIds set"() { + given: + def messageLink = new MessageLink() + String messageId = "messageId" + + when: + messageLink.addMessageId(messageId) + + then: + messageLink.getMessageIds().size() == 1 + messageLink.getMessageIds().contains(messageId) + } + + def "addMessageIds adds all messageIds to the messageIds set"() { + given: + def messageLink = new MessageLink() + def newMessageIds = Set.of("messageId1", "messageId2") + + when: + messageLink.addMessageIds(newMessageIds) + + then: + messageLink.getMessageIds().size() == 2 + messageLink.getMessageIds().containsAll(newMessageIds) + } + + def "adding message ids doesn't duplicate existing ids"() { + given: + def existingMessageId = "messageId1" + def messageIds = Set.of(existingMessageId, "messageId2") + def messageLink = new MessageLink(UUID.randomUUID(), messageIds) + + when: + messageLink.addMessageId(existingMessageId) + + then: + messageLink.getMessageIds().size() == 2 + messageLink.getMessageIds() == messageIds + + when: + messageLink.addMessageIds(messageIds) + + then: + messageLink.getMessageIds() == messageIds + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelperTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelperTest.groovy index 75cde753c..d74f44256 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelperTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/messages/SendMessageHelperTest.groovy @@ -1,6 +1,7 @@ package gov.hhs.cdc.trustedintermediary.etor.messages import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkException import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataException import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataMessageType import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataOrchestrator @@ -8,9 +9,13 @@ import gov.hhs.cdc.trustedintermediary.wrappers.Logger import spock.lang.Specification class SendMessageHelperTest extends Specification { - def mockOrchestrator = Mock(PartnerMetadataOrchestrator) def mockLogger = Mock(Logger) + private sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") + private sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") + private receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") + private receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") + private placerOrderNumber = "placer_order_number" def setup() { TestApplicationContext.reset() @@ -21,28 +26,16 @@ class SendMessageHelperTest extends Specification { TestApplicationContext.injectRegisteredImplementations() } def "savePartnerMetadataForReceivedMessage works"() { - given: - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - when: - SendMessageHelper.getInstance().savePartnerMetadataForReceivedMessage("receivedId", new Random().nextInt(), PartnerMetadataMessageType.RESULT,sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + SendMessageHelper.getInstance().savePartnerMetadataForReceivedMessage("receivedId", new Random().nextInt(), PartnerMetadataMessageType.RESULT,sendingApp, sendingFacility, receivingApp, receivingFacility, placerOrderNumber) then: 1 * mockOrchestrator.updateMetadataForReceivedMessage(_, _, _, _, _, _, _, _) } def "savePartnerMetadataForReceivedMessage should log warnings for null receivedSubmissionId"() { - given: - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - when: - SendMessageHelper.getInstance().savePartnerMetadataForReceivedMessage(null, new Random().nextInt(), PartnerMetadataMessageType.RESULT, sendingApp, sendingFacility, receivingApp, receivingFacility,"placer_order_number") + SendMessageHelper.getInstance().savePartnerMetadataForReceivedMessage(null, new Random().nextInt(), PartnerMetadataMessageType.RESULT, sendingApp, sendingFacility, receivingApp, receivingFacility, placerOrderNumber) then: 1 * mockLogger.logWarning(_) @@ -53,11 +46,6 @@ class SendMessageHelperTest extends Specification { def hashCode = new Random().nextInt() def messageType = PartnerMetadataMessageType.RESULT def receivedSubmissionId = "receivedId" - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - def placerOrderNumber = "placer_order_number" mockOrchestrator.updateMetadataForReceivedMessage(receivedSubmissionId, _ as String, messageType, sendingApp, sendingFacility, receivingApp, receivingFacility, placerOrderNumber) >> { throw new PartnerMetadataException("Error") } when: @@ -103,4 +91,64 @@ class SendMessageHelperTest extends Specification { then: 1 * mockLogger.logError(_, _) } + + def "linkMessage logs warning and ends silently when passed a null id"() { + when: + SendMessageHelper.getInstance().linkMessage(null) + + then: + 1 * mockLogger.logWarning(_, _) + notThrown(Exception) + } + + def "linkMessage logs error when there's a PartnerMetadataException"() { + given: + mockOrchestrator.findMessagesIdsToLink(_ as String) >> {throw new PartnerMetadataException("")} + + when: + SendMessageHelper.getInstance().linkMessage("1") + + then: + 1 * mockLogger.logError(_, _) + notThrown(PartnerMetadataException) + } + + def "linkMessage logs error when there's a MessageLinkException"() { + given: + mockOrchestrator.findMessagesIdsToLink(_ as String) >> ["1"] + mockOrchestrator.linkMessages(_ as Set) >> {throw new MessageLinkException("", new Exception())} + + when: + SendMessageHelper.getInstance().linkMessage("1") + + then: + 1 * mockLogger.logError(_, _) + notThrown(MessageLinkException) + } + + def "linkMessage finishes silently if the list of message ids is null"() { + given: + mockOrchestrator.findMessagesIdsToLink(_ as String) >> null + + when: + SendMessageHelper.getInstance().linkMessage("1") + + then: + 0 * mockLogger.logWarning(_, _) + 0 * mockLogger.logError(_, _) + notThrown(Exception) + } + + def "linkMessage finishes silently if the list of message ids is empty"() { + given: + mockOrchestrator.findMessagesIdsToLink(_ as String) >> [] + + when: + SendMessageHelper.getInstance().linkMessage("1") + + then: + 0 * mockLogger.logWarning(_, _) + 0 * mockLogger.logError(_, _) + notThrown(Exception) + } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestratorTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestratorTest.groovy index 3e896f2bb..9612c0eef 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestratorTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/partner/PartnerMetadataOrchestratorTest.groovy @@ -2,6 +2,8 @@ package gov.hhs.cdc.trustedintermediary.etor.metadata.partner import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext import gov.hhs.cdc.trustedintermediary.etor.RSEndpointClient +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLink +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkStorage import gov.hhs.cdc.trustedintermediary.etor.messages.MessageHdDataType import gov.hhs.cdc.trustedintermediary.etor.orders.OrderConverter import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrderConverter @@ -16,18 +18,20 @@ import spock.lang.Specification class PartnerMetadataOrchestratorTest extends Specification { private def mockPartnerMetadataStorage + private def mockMessageLinkStorage private def mockClient private def mockFormatter - private def sendingApp - private def sendingFacility - private def receivingApp - private def receivingFacility - private def placerOrderNumber + private MessageHdDataType sendingApp + private MessageHdDataType sendingFacility + private MessageHdDataType receivingApp + private MessageHdDataType receivingFacility + private String placerOrderNumber def setup() { TestApplicationContext.reset() TestApplicationContext.init() mockPartnerMetadataStorage = Mock(PartnerMetadataStorage) + mockMessageLinkStorage = Mock(MessageLinkStorage) mockFormatter = Mock(Formatter) mockClient = Mock(RSEndpointClient) @@ -38,6 +42,7 @@ class PartnerMetadataOrchestratorTest extends Specification { placerOrderNumber = "placer_order_number" TestApplicationContext.register(PartnerMetadataOrchestrator, PartnerMetadataOrchestrator.getInstance()) + TestApplicationContext.register(MessageLinkStorage, mockMessageLinkStorage) TestApplicationContext.register(OrderConverter, HapiOrderConverter.getInstance()) TestApplicationContext.register(PartnerMetadataStorage, mockPartnerMetadataStorage) @@ -121,6 +126,21 @@ class PartnerMetadataOrchestratorTest extends Specification { 0 * mockPartnerMetadataStorage.saveMetadata(_ as PartnerMetadata) } + def "updateMetadataForSentMessage ends when sentSubmissionId matches the one provided by PartnerMetadata"() { + given: + def receivedSubmissionId = "receivedSubmissionId" + def sentSubmissionId = "sentSubmissionId" + + def optional = Optional.of(new PartnerMetadata(receivedSubmissionId, sentSubmissionId,"","",Instant.now(), null, "", PartnerMetadataStatus.FAILED, null, PartnerMetadataMessageType.RESULT, sendingApp, sendingFacility, receivingApp, receivingFacility, placerOrderNumber)) + mockPartnerMetadataStorage.readMetadata(receivedSubmissionId) >> optional + + when: + PartnerMetadataOrchestrator.getInstance().updateMetadataForSentMessage(receivedSubmissionId, sentSubmissionId) + + then: + 0 * mockPartnerMetadataStorage.saveMetadata(_ as PartnerMetadata) + } + def "getMetadata returns empty Optional when data is not found"() { given: String receivedSubmissionId = "receivedSubmissionId" @@ -310,6 +330,20 @@ class PartnerMetadataOrchestratorTest extends Specification { 0 * mockClient.requestHistoryEndpoint(_, _) } + def "getMetadata skips lookup with stale metadata and missing sentSubmissionId"() { + given: + def receivedSubmissionId = "receivedSubmissionId" + def metadata = new PartnerMetadata(receivedSubmissionId, null, "sender", "receiver", Instant.now(), null, "hash", PartnerMetadataStatus.PENDING, null, PartnerMetadataMessageType.RESULT, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + + when: + PartnerMetadataOrchestrator.getInstance().getMetadata(receivedSubmissionId) + + then: + 1 * mockPartnerMetadataStorage.readMetadata(receivedSubmissionId) >> Optional.of(metadata) + 0 * mockClient.requestHistoryEndpoint(_, _) + notThrown(PartnerMetadataException) + } + def "getMetadata retrieves metadata successfully when receiver is present and sentSubmissionId is missing"() { given: def receivedSubmissionId = "receivedSubmissionId" @@ -433,6 +467,83 @@ class PartnerMetadataOrchestratorTest extends Specification { 1 * mockPartnerMetadataStorage.saveMetadata(expectedMetadata) } + def "getMetadata saves pending without delivery time if nobody has delivery times"() { + given: + def receivedSubmissionId = "receivedSubmissionId" + def sentSubmissionId = "sentSubmissionId" + def sender = "senderName" + def receiver = "org.service" + def timestamp = Instant.now() + def hashCode = "123" + def bearerToken = "token" + def rsHistoryApiResponse = "whatever" + def messageType = PartnerMetadataMessageType.RESULT + def missingReceiverMetadata = new PartnerMetadata(receivedSubmissionId, sentSubmissionId, sender, receiver, timestamp, null, hashCode, PartnerMetadataStatus.PENDING, null, messageType, sendingApp, sendingFacility, receivingApp, receivingFacility, placerOrderNumber) + + mockClient.getRsToken() >> bearerToken + mockClient.requestHistoryEndpoint(sentSubmissionId, bearerToken) >> rsHistoryApiResponse + mockFormatter.convertJsonToObject(rsHistoryApiResponse, _ as TypeReference) >> [ + overallStatus: "Pending", + actualCompletionAt: null, + destinations: [ + [organization_id: "org", service: "service"], + ], + errors: [ + [ + message: "This is an error message" + ] + ], + ] + + when: + Optional result = PartnerMetadataOrchestrator.getInstance().getMetadata(receivedSubmissionId) + + then: + result.isPresent() + result.get() == missingReceiverMetadata + 1 * mockPartnerMetadataStorage.readMetadata(receivedSubmissionId) >> Optional.of(missingReceiverMetadata) + 1 * mockPartnerMetadataStorage.saveMetadata(missingReceiverMetadata) + } + + def "getMetadata saves loaded delivered metadata if found"() { + given: + def receivedSubmissionId = "receivedSubmissionId" + def sentSubmissionId = "sentSubmissionId" + def sender = "senderName" + def receiver = "org.service" + def timestamp = Instant.now() + def hashCode = "123" + def bearerToken = "token" + def rsHistoryApiResponse = "whatever" + def messageType = PartnerMetadataMessageType.RESULT + def missingReceiverMetadata = new PartnerMetadata(receivedSubmissionId, sentSubmissionId, sender, receiver, timestamp, null, hashCode, PartnerMetadataStatus.PENDING, null, messageType, sendingApp, sendingFacility, receivingApp, receivingFacility, placerOrderNumber) + def expectedMetadata = new PartnerMetadata(receivedSubmissionId, sentSubmissionId, sender, receiver, timestamp, null, hashCode, PartnerMetadataStatus.DELIVERED, null, messageType, sendingApp, sendingFacility, receivingApp, receivingFacility, placerOrderNumber) + + mockClient.getRsToken() >> bearerToken + mockClient.requestHistoryEndpoint(sentSubmissionId, bearerToken) >> rsHistoryApiResponse + mockFormatter.convertJsonToObject(rsHistoryApiResponse, _ as TypeReference) >> [ + overallStatus: "Delivered", + actualCompletionAt: null, + destinations: [ + [organization_id: "org", service: "service"], + ], + errors: [ + [ + message: "This is an error message" + ] + ], + ] + + when: + Optional result = PartnerMetadataOrchestrator.getInstance().getMetadata(receivedSubmissionId) + + then: + result.isPresent() + result.get() == expectedMetadata + 1 * mockPartnerMetadataStorage.readMetadata(receivedSubmissionId) >> Optional.of(missingReceiverMetadata) + 1 * mockPartnerMetadataStorage.saveMetadata(expectedMetadata) + } + def "setMetadataStatusToFailed sets status to Failed"() { given: def submissionId = "13425" @@ -507,6 +618,13 @@ class PartnerMetadataOrchestratorTest extends Specification { def "getDataFromReportStream throws FormatterProcessingException or returns null for unexpected format response"() { given: + def exception + def objectMapperMessage = "objectMapper failed to convert" + def noReceiverMessage = "Unable to extract receiver name" + def noStatusMessage = "Unable to extract overallStatus" + def noReasonMessage = "Unable to extract failure reason" + def noTimeMessage = "Unable to extract timeDelivered" + TestApplicationContext.register(Formatter, Jackson.getInstance()) TestApplicationContext.injectRegisteredImplementations() @@ -515,24 +633,26 @@ class PartnerMetadataOrchestratorTest extends Specification { PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(invalidJson) then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(objectMapperMessage) >= 0 when: def emptyJson = "{}" PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(emptyJson) then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(noReceiverMessage) >= 0 when: def jsonWithoutDestinations = "{\"someotherkey\": \"value\"}" PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithoutDestinations) then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(noReceiverMessage) >= 0 when: - def jsonWithEmptyDestinations = """{"destinations": [], "errors": []}""" def parsedData = PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithEmptyDestinations) @@ -540,7 +660,6 @@ class PartnerMetadataOrchestratorTest extends Specification { parsedData[0] == null when: - def jsonWithNoStatus = """{"destinations": [], "errors": []}""" parsedData = PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithNoStatus) @@ -552,32 +671,40 @@ class PartnerMetadataOrchestratorTest extends Specification { PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithoutOrgId) then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(noReceiverMessage) >= 0 when: def jsonWithoutService = "{\"destinations\":[{\"organization_id\":\"org_id\"}]}" PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithoutService) then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(noReceiverMessage) >= 0 when: def jsonWithoutErrorMessageSubString = "{\"destinations\":[{\"organization_id\":\"org_id\", \"service\":\"service\"}], \"overallStatus\": \"Error\"}" PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithoutErrorMessageSubString) then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(noReasonMessage) >= 0 when: - def jsonWithBadCompletionDate = "{\"actualCompletionAt\": 123, \"destinations\":[{\"organization_id\":\"org_id\", \"service\":\"service\"}], \"overallStatus\": \"Error\"}" + def jsonWithBadCompletionDate = "{\"actualCompletionAt\": 123, \"destinations\":[{\"organization_id\":\"org_id\", \"service\":\"service\"}], \"overallStatus\": \"Error\", \"errors\": []}" PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithBadCompletionDate) + then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(noTimeMessage) >= 0 + when: def jsonWithBadStatus = "{\"overallStatus\": 123, \"destinations\":[{\"organization_id\":\"org_id\", \"service\":\"service\"}]}" PartnerMetadataOrchestrator.getInstance().getDataFromReportStream(jsonWithBadStatus) + then: - thrown(FormatterProcessingException) + exception = thrown(FormatterProcessingException) + exception.getMessage().indexOf(noStatusMessage) >= 0 } def "ourStatusFromReportStreamStatus returns FAILED"() { @@ -642,4 +769,82 @@ class PartnerMetadataOrchestratorTest extends Specification { result["123456789"]["stale"] == true result["123456789"]["failureReason"] == failure } + + def "findMessagesIdsToLink returns a list of message ids"() { + given: + def receivedSubmissionId = "receivedSubmissionId" + def placerOrderNumber = "placerOrderNumber" + def receivedSubmissionId1 = "1" + def receivedSubmissionId2 = "2" + def sendingAppDetails = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") + def sendingFacilityDetails = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") + def receivingAppDetails = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") + def receivingFacilityDetails = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") + def partnerMetadata1 = new PartnerMetadata(receivedSubmissionId1, "sender1", Instant.now(), null, "hash1", PartnerMetadataStatus.DELIVERED, PartnerMetadataMessageType.ORDER, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, placerOrderNumber) + def partnerMetadata2 = new PartnerMetadata(receivedSubmissionId2, "sender2", Instant.now(), null, "hash2", PartnerMetadataStatus.DELIVERED, PartnerMetadataMessageType.RESULT, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, placerOrderNumber) + def metadataSetForMessageLinking = Set.of(partnerMetadata1, partnerMetadata2) + mockPartnerMetadataStorage.readMetadataForMessageLinking(receivedSubmissionId) >> metadataSetForMessageLinking + + when: + def result = PartnerMetadataOrchestrator.getInstance().findMessagesIdsToLink(receivedSubmissionId) + + then: + result == Set.of(receivedSubmissionId1, receivedSubmissionId2) + } + + def "linkMessages links messages successfully"() { + given: + def matchingMessageId = "matchingMessageId" + def additionalMessageId = "additionalMessageId" + def newMessageId = "newMessageId" + def messageIdsToLink = Set.of(matchingMessageId, newMessageId) + def existingLinkId = UUID.randomUUID() + def existingMessageLink = new MessageLink(existingLinkId, Set.of(matchingMessageId, additionalMessageId)) + mockMessageLinkStorage.getMessageLink(newMessageId) >> Optional.empty() + + when: + PartnerMetadataOrchestrator.getInstance().linkMessages(messageIdsToLink) + + then: + 1 * mockMessageLinkStorage.getMessageLink(matchingMessageId) >> Optional.of(existingMessageLink) + existingMessageLink.addMessageId(newMessageId) + 1 * mockMessageLinkStorage.saveMessageLink(existingMessageLink) + } + + def "linkMessages creates new message link there is no existing link"() { + given: + def messageId1 = "messageId1" + def messageId2 = "messageId2" + def messageIdsToLink = Set.of(messageId1, messageId2) + mockMessageLinkStorage.getMessageLink(messageId1) >> Optional.empty() + mockMessageLinkStorage.getMessageLink(messageId2) >> Optional.empty() + + when: + PartnerMetadataOrchestrator.getInstance().linkMessages(messageIdsToLink) + + then: + 1 * mockMessageLinkStorage.saveMessageLink({ MessageLink ml -> + ml.getLinkId() != null && ml.getMessageIds() == messageIdsToLink + }) + } + + def "linkMessages uses existing link if one exists"() { + given: + def existingLinkId = UUID.randomUUID() + def matchingMessageId = "messageId" + def additionalMessageId = "additionalMessageId" + def newMessageId = "newMessageId" + def messageIdsToLink = Set.of(matchingMessageId, newMessageId) + def existingMessageLink = new MessageLink(existingLinkId, Set.of(matchingMessageId, additionalMessageId)) + mockMessageLinkStorage.getMessageLink(matchingMessageId) >> Optional.of(existingMessageLink) + mockMessageLinkStorage.getMessageLink(newMessageId) >> Optional.empty() + + when: + PartnerMetadataOrchestrator.getInstance().linkMessages(messageIdsToLink) + + then: + 1 * mockMessageLinkStorage.saveMessageLink({ MessageLink ml -> + ml.getLinkId() == existingLinkId && ml.getMessageIds() == Set.of(matchingMessageId, additionalMessageId, newMessageId) + }) + } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCaseTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCaseTest.groovy index d6c3c98f9..b2ddcd73a 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCaseTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCaseTest.groovy @@ -36,6 +36,7 @@ class SendOrderUseCaseTest extends Specification { def receivedSubmissionId = "receivedId" def sentSubmissionId = "sentId" def messageType = PartnerMetadataMessageType.ORDER + def messagesIdsToLink = Set.of("messageId1", "messageId2") def sendOrder = SendOrderUseCase.getInstance() def mockOrder = new OrderMock(null, null, null, null, null, null, null, null) @@ -58,12 +59,14 @@ class SendOrderUseCaseTest extends Specification { receivedSubmissionId, _ as String, messageType, - mockOrder.getSendingApplicationDetails(), - mockOrder.getSendingFacilityDetails(), - mockOrder.getReceivingApplicationDetails(), - mockOrder.getReceivingFacilityDetails(), - mockOrder.getPlacerOrderNumber()) + mockOmlOrder.getSendingApplicationDetails(), + mockOmlOrder.getSendingFacilityDetails(), + mockOmlOrder.getReceivingApplicationDetails(), + mockOmlOrder.getReceivingFacilityDetails(), + mockOmlOrder.getPlacerOrderNumber()) 1 * mockOrchestrator.updateMetadataForSentMessage(receivedSubmissionId, sentSubmissionId) + 1 * mockOrchestrator.findMessagesIdsToLink(receivedSubmissionId) >> messagesIdsToLink + 1 * mockOrchestrator.linkMessages(messagesIdsToLink) } def "send fails to send"() { @@ -87,7 +90,7 @@ class SendOrderUseCaseTest extends Specification { SendOrderUseCase.getInstance().convertAndSend(Mock(Order), null) then: - 2 * mockLogger.logWarning(_) + 3 * mockLogger.logWarning(_) 0 * mockOrchestrator.updateMetadataForReceivedMessage(_, _) } @@ -113,6 +116,7 @@ class SendOrderUseCaseTest extends Specification { 1 * mockConverter.convertToOmlOrder(order) >> omlOrder 1 * mockConverter.addContactSectionToPatientResource(omlOrder) >> omlOrder 1 * mockConverter.addEtorProcessingTag(omlOrder) >> omlOrder + 1 * mockOrchestrator.findMessagesIdsToLink(receivedSubmissionId) >> Set.of() 1 * mockSender.send(omlOrder) >> Optional.of("sentId") } @@ -131,6 +135,7 @@ class SendOrderUseCaseTest extends Specification { 1 * mockConverter.convertToOmlOrder(order) >> omlOrder 1 * mockConverter.addContactSectionToPatientResource(omlOrder) >> omlOrder 1 * mockConverter.addEtorProcessingTag(omlOrder) >> omlOrder + 1 * mockOrchestrator.findMessagesIdsToLink(_ as String) >> Set.of() 1 * mockSender.send(omlOrder) >> Optional.of("sentId") 1 * mockLogger.logError(_, partnerMetadataException) } @@ -147,6 +152,7 @@ class SendOrderUseCaseTest extends Specification { then: 1 * mockLogger.logWarning(_) + 1 * mockOrchestrator.findMessagesIdsToLink(_ as String) >> Set.of() 0 * mockOrchestrator.updateMetadataForSentMessage(_ as String, _ as String) } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCaseTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCaseTest.groovy index 7a32b7846..7ecc1f3ab 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCaseTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/results/SendResultUseCaseTest.groovy @@ -62,8 +62,7 @@ class SendResultUseCaseTest extends Specification { def result = Mock(Result) def receivedSubmissionId = "receivedId" def messageType = PartnerMetadataMessageType.RESULT - mockOrchestrator.updateMetadataForReceivedMessage(receivedSubmissionId, _ as String, messageType, - result.getSendingApplicationDetails(), + mockOrchestrator.updateMetadataForReceivedMessage(receivedSubmissionId, _ as String, messageType,result.getSendingApplicationDetails(), result.getSendingFacilityDetails(), result.getReceivingApplicationDetails(), result.getReceivingFacilityDetails(), diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/DatabaseMessageLinkStorageTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/DatabaseMessageLinkStorageTest.groovy new file mode 100644 index 000000000..0fa9a2cef --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/DatabaseMessageLinkStorageTest.groovy @@ -0,0 +1,144 @@ +package gov.hhs.cdc.trustedintermediary.external.database + +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLink +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkException +import gov.hhs.cdc.trustedintermediary.wrappers.database.ConnectionPool + +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException +import spock.lang.Specification + +class DatabaseMessageLinkStorageTest extends Specification { + + private ConnectionPool mockConnPool + private DbDao mockDao + + def mockMessageLinkData = new MessageLink(UUID.randomUUID(), "TestMessageId") + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + + mockDao = Mock(DbDao) + mockConnPool = Mock(ConnectionPool) + + TestApplicationContext.register(DbDao, mockDao) + TestApplicationContext.register(ConnectionPool, mockConnPool) + TestApplicationContext.register(DatabaseMessageLinkStorage, DatabaseMessageLinkStorage.getInstance()) + TestApplicationContext.injectRegisteredImplementations() + } + + + def "getMessageLink returns message link when rows exist"() { + given: + def linkId = UUID.randomUUID() + def getMessageId = "getMessageId" + def additionalMessageId = "additionalMessageId" + def messageLink = new MessageLink(linkId, Set.of(getMessageId, additionalMessageId)) + def expected = Optional.of(messageLink) + + def mockConn = Mock(Connection) + def mockPreparedStatement = Mock(PreparedStatement) + def mockResultSet = Mock(ResultSet) + + mockConnPool.getConnection() >> mockConn + mockConn.prepareStatement(_ as String) >> mockPreparedStatement + mockResultSet.next() >> true >> true >> false + + mockPreparedStatement.executeQuery() >> mockResultSet + + TestApplicationContext.register(ConnectionPool, mockConnPool) + TestApplicationContext.injectRegisteredImplementations() + + when: + def actual = DatabaseMessageLinkStorage.getInstance().getMessageLink(getMessageId) + + then: + 1 * mockResultSet.getString("link_id") >> linkId + 1 * mockResultSet.getString("message_id") >> getMessageId + 1 * mockResultSet.getString("message_id") >> additionalMessageId + actual.get().getLinkId() == expected.get().getLinkId() + } + + + def "getMessageLink returns empty optional when rows do not exist"() { + given: + def mockConn = Mock(Connection) + def mockPreparedStatement = Mock(PreparedStatement) + def mockResultSet = Mock(ResultSet) + + mockConnPool.getConnection() >> mockConn + mockConn.prepareStatement(_ as String) >> mockPreparedStatement + mockResultSet.next() >> false + mockPreparedStatement.executeQuery() >> mockResultSet + + TestApplicationContext.register(ConnectionPool, mockConnPool) + TestApplicationContext.injectRegisteredImplementations() + + when: + def actual = DatabaseMessageLinkStorage.getInstance().getMessageLink("mock_lookup") + + then: + actual == Optional.empty() + } + + def "getMessageLink throws SQLException if something goes wrong"() { + given: + def mockConn + def mockPreparedStatement + TestApplicationContext.register(ConnectionPool, mockConnPool) + TestApplicationContext.injectRegisteredImplementations() + + when: + mockConn = Mock(Connection) + mockPreparedStatement = Mock(PreparedStatement) + mockConnPool.getConnection() >> mockConn + mockConn.prepareStatement(_ as String) >> mockPreparedStatement + mockPreparedStatement.executeQuery() >> { throw new SQLException("Something went wrong!") } + DatabaseMessageLinkStorage.getInstance().getMessageLink("TestSubmissionId") + + then: + thrown(Exception) + + when: + mockConn = Mock(Connection) + mockConnPool.getConnection() >> mockConn + mockConn.prepareStatement(_ as String) >> { throw new SQLException("Something went wrong!") } + DatabaseMessageLinkStorage.getInstance().getMessageLink("TestSubmissionId") + + then: + thrown(Exception) + + when: + mockConnPool.getConnection() >> { throw new SQLException("Something went wrong!") } + DatabaseMessageLinkStorage.getInstance().getMessageLink("TestSubmissionId") + + then: + thrown(Exception) + } + + def "saveLinkedMessages happy path works"() { + given: + def messageIdCount = mockMessageLinkData.getMessageIds().size() + + when: + DatabaseMessageLinkStorage.getInstance().saveMessageLink(mockMessageLinkData) + + then: + messageIdCount * mockDao.upsertData("message_link", _ as List, _ as String) + } + + def "saveMessageLink unhappy path works"() { + given: + mockDao.upsertData("message_link", _ as List, _ as String) >> { throw new SQLException("Something went wrong!") } + + when: + DatabaseMessageLinkStorage.getInstance().saveMessageLink(mockMessageLinkData) + + then: + thrown(MessageLinkException) + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorageTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorageTest.groovy index 6b8920304..de4cd3663 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorageTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/DatabasePartnerMetadataStorageTest.groovy @@ -23,11 +23,11 @@ class DatabasePartnerMetadataStorageTest extends Specification { private def mockDao private def mockFormatter - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - def mockMetadata = new PartnerMetadata("receivedSubmissionId", "sentSubmissionId","sender", "receiver", Instant.now(), Instant.now(), "hash", PartnerMetadataStatus.DELIVERED, "failure reason", PartnerMetadataMessageType.ORDER, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + def sendingAppDetails = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") + def sendingFacilityDetails = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") + def receivingAppDetails = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") + def receivingFacilityDetails = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") + def mockMetadata = new PartnerMetadata("receivedSubmissionId", "sentSubmissionId","sender", "receiver", Instant.now(), Instant.now(), "hash", PartnerMetadataStatus.DELIVERED, "failure reason", PartnerMetadataMessageType.ORDER, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number") def setup() { TestApplicationContext.reset() @@ -148,7 +148,7 @@ class DatabasePartnerMetadataStorageTest extends Specification { DatabasePartnerMetadataStorage.getInstance().saveMetadata(mockMetadata) then: - 1 * mockDao.upsertData("metadata", columns, "received_message_id") + 1 * mockDao.upsertData("metadata", columns, "(received_message_id)") } def "saveMetadata unhappy path works"() { @@ -234,10 +234,10 @@ class DatabasePartnerMetadataStorageTest extends Specification { null, // PartnerMetadata defaults deliveryStatus to PENDING on null, so that's why we're asserting not-null bellow "DogCow failure", null, - sendingApp, - sendingFacility, - receivingApp, - receivingFacility, + sendingAppDetails, + sendingFacilityDetails, + receivingAppDetails, + receivingFacilityDetails, "placer_order_number" ) @@ -266,6 +266,54 @@ class DatabasePartnerMetadataStorageTest extends Specification { DatabasePartnerMetadataStorage.getInstance().saveMetadata(mockMetadata) then: - 1 * mockDao.upsertData("metadata", columns, "received_message_id") + 1 * mockDao.upsertData("metadata", columns, "(received_message_id)") + } + + def "readMetadataForMessageLinking happy path works"() { + given: + def expectedResult = Set.of(mockMetadata) + + mockDao.fetchMetadataForMessageLinking(_ as String) >> expectedResult + + when: + def actualResult = DatabasePartnerMetadataStorage.getInstance().readMetadataForMessageLinking(mockMetadata.receivedSubmissionId()) + + then: + actualResult == expectedResult + } + + def "readMetadataForMessageLinking unhappy path works"() { + given: + mockDao.fetchMetadataForMessageLinking(_ as String) >> { throw new SQLException("Something went wrong!") } + + when: + DatabasePartnerMetadataStorage.getInstance().readMetadataForMessageLinking("receivedSubmissionId") + + then: + thrown(PartnerMetadataException) + } + + def "readMetadataForSender happy path works"() { + given: + def expectedResult = Set.of(mockMetadata) + + mockDao.fetchMetadataForSender(_ as String) >> expectedResult + + when: + def actualResult = DatabasePartnerMetadataStorage.getInstance().readMetadataForSender("TestSender") + + then: + actualResult == expectedResult + } + + def "readMetadataForSender unhappy path works"() { + given: + mockDao.fetchMetadataForSender(_ as String) >> { throw new SQLException("Something went wrong!") } + + when: + DatabasePartnerMetadataStorage.getInstance().readMetadataForSender("TestSender") + + then: + thrown(PartnerMetadataException) } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy index f517608b1..a3545c9ce 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy @@ -73,7 +73,7 @@ class PostgresDaoTest extends Specification { new DbColumn("receiving_application_details", receivingApp, false, Types.OTHER), new DbColumn("receiving_facility_details", receivingFacility, false, Types.OTHER), ] - def conflictColumnName = pkColumnName + def conflictTarget = "(" + pkColumnName + ")" mockConnPool.getConnection() >> mockConn @@ -81,7 +81,7 @@ class PostgresDaoTest extends Specification { TestApplicationContext.injectRegisteredImplementations() when: - PostgresDao.getInstance().upsertData(tableName, columns, conflictColumnName) + PostgresDao.getInstance().upsertData(tableName, columns, conflictTarget) then: mockConn.prepareStatement(_ as String) >> { String sqlStatement -> @@ -117,8 +117,9 @@ class PostgresDaoTest extends Specification { 1 * mockPreparedStatement.executeUpdate() } - def "upsertData doesn't do any upserts if there is no upsertOverwrite"() { + def "upsertData doesn't do any upserts if there is no upsertOverwrite and does nothing if conflict target is defined"() { given: + def conflictTarget def tableName = "DogCow" def columns = [ new DbColumn("Moof", "Clarus", false, Types.VARCHAR), @@ -135,18 +136,35 @@ class PostgresDaoTest extends Specification { TestApplicationContext.injectRegisteredImplementations() when: - PostgresDao.getInstance().upsertData(tableName, columns, null) + conflictTarget = null + PostgresDao.getInstance().upsertData(tableName, columns, conflictTarget) then: mockConn.prepareStatement(_ as String) >> { String sqlStatement -> assert sqlStatement.contains(tableName) assert sqlStatement.count("?") == columns.size() assert !sqlStatement.contains("ON CONFLICT") - assert !sqlStatement.contains("EXCLUDED") return mockPreparedStatement } - 6 * mockPreparedStatement.setObject(_ as Integer, _, _ as Integer) + 6 * mockPreparedStatement.setObject(_ as Integer, _, _ as Integer) + 1 * mockPreparedStatement.executeUpdate() + + when: + conflictTarget = "ON CONSTRAINT key" + PostgresDao.getInstance().upsertData(tableName, columns, conflictTarget) + + then: + mockConn.prepareStatement(_ as String) >> { String sqlStatement -> + assert sqlStatement.contains(tableName) + assert sqlStatement.count("?") == columns.size() + assert sqlStatement.contains("ON CONFLICT") + assert sqlStatement.contains(conflictTarget) + assert sqlStatement.contains("DO NOTHING") + + return mockPreparedStatement + } + 6 * mockPreparedStatement.setObject(_ as Integer, _, _ as Integer) 1 * mockPreparedStatement.executeUpdate() } @@ -379,6 +397,144 @@ class PostgresDaoTest extends Specification { actual.containsAll(Set.of(expected1, expected2)) } + def "fetchMetadataForMessageLinking returns a set of PartnerMetadata when rows exist"() { + given: + def submissionId = "12345" + def expectedMetadataSet = new HashSet() + def sender = "DogCow" + def messageType = PartnerMetadataMessageType.RESULT + def partnerMetadata1 = new PartnerMetadata("12345", "7890", sender, "You'll get your just reward", + Instant.parse("2024-01-03T15:45:33.30Z"), Instant.parse("2024-01-03T15:45:33.30Z"), sender.hashCode().toString(), + PartnerMetadataStatus.PENDING, "It done Goofed", messageType, sendingApp, sendingFacility, + receivingApp, receivingFacility, "placer_order_number") + def partnerMetadata2 = new PartnerMetadata("doreyme", "fasole", sender, "receiver", + Instant.now(), Instant.now(), "gobeltygoook", + PartnerMetadataStatus.DELIVERED, "cause I said so", messageType, sendingApp, sendingFacility, + receivingApp, receivingFacility, "placer_order_number") + expectedMetadataSet.add(partnerMetadata1) + expectedMetadataSet.add(partnerMetadata2) + + mockConnPool.getConnection() >> mockConn + mockConn.prepareStatement(_ as String) >> mockPreparedStatement + mockPreparedStatement.executeQuery() >> mockResultSet + mockResultSet.next() >> true >> true >> false + + mockResultSet.getString("received_message_id") >>> [ + partnerMetadata1.receivedSubmissionId(), + partnerMetadata2.receivedSubmissionId() + ] + mockResultSet.getString("sent_message_id") >>> [ + partnerMetadata1.sentSubmissionId(), + partnerMetadata2.sentSubmissionId() + ] + mockResultSet.getString("sender") >>> [ + partnerMetadata1.sender(), + partnerMetadata2.sender() + ] + mockResultSet.getString("receiver") >>> [ + partnerMetadata1.receiver(), + partnerMetadata2.receiver() + ] + mockResultSet.getTimestamp("time_received") >>> [ + Timestamp.from(partnerMetadata1.timeReceived()), + Timestamp.from(partnerMetadata2.timeReceived()) + ] + mockResultSet.getTimestamp("time_delivered") >>> [ + Timestamp.from(partnerMetadata1.timeDelivered()), + Timestamp.from(partnerMetadata2.timeDelivered()) + ] + mockResultSet.getString("hash_of_message") >>> [ + partnerMetadata1.hash(), + partnerMetadata2.hash() + ] + mockResultSet.getString("delivery_status") >>> [ + partnerMetadata1.deliveryStatus().toString(), + partnerMetadata2.deliveryStatus().toString() + ] + mockResultSet.getString("failure_reason") >>> [ + partnerMetadata1.failureReason(), + partnerMetadata2.failureReason() + ] + mockResultSet.getString("message_type") >>> [ + partnerMetadata1.messageType().toString(), + partnerMetadata2.messageType().toString() + ] + mockResultSet.getString("sending_application_id") >>> [ + partnerMetadata1.sendingApplicationDetails(), + partnerMetadata2.sendingApplicationDetails() + ] + mockResultSet.getString("sending_facility_id") >>> [ + partnerMetadata1.sendingFacilityDetails(), + partnerMetadata2.sendingFacilityDetails() + ] + mockResultSet.getString("receiving_application_id") >>> [ + partnerMetadata1.receivingApplicationDetails(), + partnerMetadata2.receivingApplicationDetails() + ] + mockResultSet.getString("receiving_facility_id") >>> [ + partnerMetadata1.receivingFacilityDetails(), + partnerMetadata2.receivingFacilityDetails() + ] + mockResultSet.getString("placer_order_number") >>> [ + partnerMetadata1.placerOrderNumber(), + partnerMetadata2.placerOrderNumber() + ] + + mockFormatter.convertJsonToObject(_ as String, _ as TypeReference) >>> [ + partnerMetadata1.sendingApplicationDetails(), + partnerMetadata1.sendingFacilityDetails(), + partnerMetadata1.receivingApplicationDetails(), + partnerMetadata1.receivingFacilityDetails(), + partnerMetadata2.sendingApplicationDetails(), + partnerMetadata2.sendingFacilityDetails(), + partnerMetadata2.receivingApplicationDetails(), + partnerMetadata2.receivingFacilityDetails(), + ] + + TestApplicationContext.register(ConnectionPool, mockConnPool) + TestApplicationContext.injectRegisteredImplementations() + + when: + def actualSet = PostgresDao.getInstance().fetchMetadataForMessageLinking(submissionId) + + then: + actualSet == expectedMetadataSet + } + + def "fetchMetadataForMessageLinking returns empty set when no rows exist"() { + given: + def submissionId = "noSuchId" + mockConnPool.getConnection() >> mockConn + mockConn.prepareStatement(_ as String) >> mockPreparedStatement + mockPreparedStatement.executeQuery() >> mockResultSet + mockResultSet.next() >> false // Simulate no rows found + + TestApplicationContext.register(ConnectionPool, mockConnPool) + TestApplicationContext.injectRegisteredImplementations() + + when: + def actualSet = PostgresDao.getInstance().fetchMetadataForMessageLinking(submissionId) + + then: + actualSet.isEmpty() + } + + def "fetchMetadataForMessageLinking throws SQLException on database error"() { + given: + def submissionId = "willThrowException" + mockConnPool.getConnection() >> mockConn + mockConn.prepareStatement(_ as String) >> { throw new SQLException("Database error") } + + TestApplicationContext.register(ConnectionPool, mockConnPool) + TestApplicationContext.injectRegisteredImplementations() + + when: + PostgresDao.getInstance().fetchMetadataForMessageLinking(submissionId) + + then: + thrown(SQLException) + } + def "fetchMetadata throws exception for FormatterProcessingException"() { given: mockConnPool.getConnection() >> mockConn diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy index f27d6eb8e..c9dd84366 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiMetadataConverterTest.groovy @@ -10,6 +10,11 @@ import spock.lang.Specification import java.time.Instant class HapiMetadataConverterTest extends Specification { + def sendingAppDetails = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") + def sendingFacilityDetails = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") + def receivingAppDetails = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") + def receivingFacilityDetails = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") + def "creating an issue returns a valid OperationOutcomeIssueComponent with Information level severity and code" () { when: def output = HapiPartnerMetadataConverter.getInstance().createInformationIssueComponent("test_details", "test_diagnostics") @@ -29,12 +34,8 @@ class HapiMetadataConverterTest extends Specification { def hash = "hash" def failureReason = "timed_out" def messageType = PartnerMetadataMessageType.ORDER - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") PartnerMetadata metadata = new PartnerMetadata( - "receivedSubmissionId", "sentSubmissionId", sender, receiver, time, time, hash, PartnerMetadataStatus.DELIVERED, failureReason, messageType, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + "receivedSubmissionId", "sentSubmissionId", sender, receiver, time, time, hash, PartnerMetadataStatus.DELIVERED, failureReason, messageType, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number") when: def result = HapiPartnerMetadataConverter.getInstance().extractPublicMetadataToOperationOutcome(metadata, "receivedSubmissionId").getUnderlyingOutcome() as OperationOutcome diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FileMessageLinkStorageTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FileMessageLinkStorageTest.groovy new file mode 100644 index 000000000..ecea4f6b3 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FileMessageLinkStorageTest.groovy @@ -0,0 +1,137 @@ +package gov.hhs.cdc.trustedintermediary.external.localfile + +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLink +import gov.hhs.cdc.trustedintermediary.etor.messagelink.MessageLinkException +import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson +import gov.hhs.cdc.trustedintermediary.wrappers.Logger +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference +import spock.lang.Specification + +class FileMessageLinkStorageTest extends Specification { + + def messageLinkStorage = FileMessageLinkStorage.getInstance() + def mockLogger = Mock(Logger) + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(FileMessageLinkStorage, messageLinkStorage) + TestApplicationContext.register(Logger, mockLogger) + } + + def "save and read message links successfully"() { + given: + TestApplicationContext.register(Formatter, Jackson.getInstance()) + TestApplicationContext.injectRegisteredImplementations() + + when: + def messageId1 = "messageId1" + var expectedMessageLink1 = new MessageLink(UUID.randomUUID(), Set.of(messageId1, "additionalMessageId1")) + messageLinkStorage.saveMessageLink(expectedMessageLink1) + def actualMessageLink1 = messageLinkStorage.getMessageLink(messageId1) + + then: + actualMessageLink1.isPresent() + with(actualMessageLink1.get()) { + linkId == expectedMessageLink1.linkId + messageIds.containsAll(expectedMessageLink1.messageIds) && expectedMessageLink1.messageIds.containsAll(messageIds) + } + 0 * mockLogger.logWarning(_ as String) + + when: + def messageId2 = "messageId2" + var expectedMessageLink2 = new MessageLink(UUID.randomUUID(), Set.of(messageId2, "additionalMessageId2")) + messageLinkStorage.saveMessageLink(expectedMessageLink2) + def actualMessageLink2 = messageLinkStorage.getMessageLink(messageId2) + + then: + actualMessageLink2.isPresent() + with(actualMessageLink2.get()) { + linkId == expectedMessageLink2.linkId + messageIds.containsAll(expectedMessageLink2.messageIds) && expectedMessageLink2.messageIds.containsAll(messageIds) + } + 0 * mockLogger.logWarning(_ as String) + } + + def "getMessageLink throws MessageLinkException when unable to parse file"() { + given: + def mockFormatter = Mock(Formatter) + mockFormatter.convertToJsonString(_) >> "[]" + mockFormatter.convertJsonToObject(_ as String, _ as TypeReference) >> null >> {throw new FormatterProcessingException("error", new Exception())} + TestApplicationContext.register(Formatter, mockFormatter) + TestApplicationContext.injectRegisteredImplementations() + + def submissionId = "submissionId" + def messageLink = new MessageLink(UUID.randomUUID(), Set.of(submissionId, "messageId2")) + messageLinkStorage.saveMessageLink(messageLink) + + when: + messageLinkStorage.getMessageLink(submissionId) + + then: + thrown(MessageLinkException) + } + + def "getMessageLink logs a warning when more than one message link is found for a message id"() { + given: + def repeatedMessageId = "messageId1" + def messageLink1 = new MessageLink(UUID.randomUUID(), Set.of(repeatedMessageId, "messageId2")) + def messageLink2 = new MessageLink(UUID.randomUUID(), Set.of(repeatedMessageId, "messageId3")) + + def mockFormatter = Mock(Formatter) + mockFormatter.convertJsonToObject(_ as String, _ as TypeReference) >> Set.of(messageLink1, messageLink2) + TestApplicationContext.register(Formatter, mockFormatter) + TestApplicationContext.injectRegisteredImplementations() + + when: + messageLinkStorage.getMessageLink(repeatedMessageId) + + then: + 1 * mockLogger.logWarning(_ as String, repeatedMessageId) + } + + def "saveMessageLink throws MessageLinkException when unable to save file"() { + given: + def messageLink = new MessageLink(UUID.randomUUID(), Set.of("messageId1", "messageId2")) + + def mockFormatter = Mock(Formatter) + mockFormatter.convertToJsonString(Set.of(messageLink)) >> {throw new FormatterProcessingException("error", new Exception())} + TestApplicationContext.register(Formatter, mockFormatter) + TestApplicationContext.injectRegisteredImplementations() + + when: + messageLinkStorage.saveMessageLink(messageLink) + + then: + thrown(MessageLinkException) + } + + def "saveMessageLink adds messageIds to existing message link"() { + given: + def linkId = UUID.randomUUID() + def messageId = "messageId" + def existingMessageIds = Set.of(messageId, "messageId2") + def existingMessageLink = new MessageLink(linkId, existingMessageIds) + def newMessageIds = Set.of(messageId, "messageId3") + def newMessageLink = new MessageLink(linkId, newMessageIds) + + TestApplicationContext.register(Formatter, Jackson.getInstance()) + TestApplicationContext.injectRegisteredImplementations() + + messageLinkStorage.saveMessageLink(existingMessageLink) + + when: + messageLinkStorage.saveMessageLink(newMessageLink) + + then: + def mergedMessageLink = messageLinkStorage.getMessageLink(messageId) + mergedMessageLink.isPresent() + with(mergedMessageLink.get()) { + linkId == linkId + messageIds.containsAll(existingMessageLink.messageIds) && messageIds.containsAll(newMessageLink.messageIds) + } + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy index 11697c239..70ac861b8 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy @@ -15,6 +15,10 @@ import java.time.Instant import spock.lang.Specification class FilePartnerMetadataStorageTest extends Specification { + def sendingAppDetails = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") + def sendingFacilityDetails = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") + def receivingAppDetails = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") + def receivingFacilityDetails = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") def setup() { TestApplicationContext.reset() @@ -28,11 +32,7 @@ class FilePartnerMetadataStorageTest extends Specification { given: def expectedReceivedSubmissionId = "receivedSubmissionId" def expectedSentSubmissionId = "receivedSubmissionId" - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - PartnerMetadata metadata = new PartnerMetadata(expectedReceivedSubmissionId, expectedSentSubmissionId, "sender", "receiver", Instant.parse("2023-12-04T18:51:48.941875Z"),Instant.parse("2023-12-04T18:51:48.941875Z"), "abcd", PartnerMetadataStatus.DELIVERED, null, PartnerMetadataMessageType.ORDER, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + PartnerMetadata metadata = new PartnerMetadata(expectedReceivedSubmissionId, expectedSentSubmissionId, "sender", "receiver", Instant.parse("2023-12-04T18:51:48.941875Z"),Instant.parse("2023-12-04T18:51:48.941875Z"), "abcd", PartnerMetadataStatus.DELIVERED, null, PartnerMetadataMessageType.ORDER, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number") TestApplicationContext.register(Formatter, Jackson.getInstance()) TestApplicationContext.injectRegisteredImplementations() @@ -47,11 +47,7 @@ class FilePartnerMetadataStorageTest extends Specification { def "saveMetadata throws PartnerMetadataException when unable to save file"() { given: - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - PartnerMetadata metadata = new PartnerMetadata("receivedSubmissionId", "sentSubmissionId","sender", "receiver", Instant.now(), Instant.now(), "abcd", PartnerMetadataStatus.DELIVERED, null, PartnerMetadataMessageType.ORDER, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + PartnerMetadata metadata = new PartnerMetadata("receivedSubmissionId", "sentSubmissionId","sender", "receiver", Instant.now(), Instant.now(), "abcd", PartnerMetadataStatus.DELIVERED, null, PartnerMetadataMessageType.ORDER, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number") def mockFormatter = Mock(Formatter) mockFormatter.convertToJsonString(_ as PartnerMetadata) >> {throw new FormatterProcessingException("error", new Exception())} @@ -75,11 +71,7 @@ class FilePartnerMetadataStorageTest extends Specification { //write something to the hard drive so that the `readMetadata` in the when gets pass the file existence check def submissionId = "asljfaskljgalsjgjlas" - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - PartnerMetadata metadata = new PartnerMetadata(submissionId, null, null, null, null, null, null, null, null, null, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + PartnerMetadata metadata = new PartnerMetadata(submissionId, null, null, null, null, null, null, null, null, null, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number") FilePartnerMetadataStorage.getInstance().saveMetadata(metadata) when: @@ -100,12 +92,8 @@ class FilePartnerMetadataStorageTest extends Specification { def "readMetadataForSender returns a set of PartnerMetadata"() { given: def sender = "same_sender" - def sendingApp = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") - def sendingFacility = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") - def receivingApp = new MessageHdDataType("receiving_app_name", "receiving_app_id", "receiving_app_type") - def receivingFacility = new MessageHdDataType("receiving_facility_name", "receiving_facility_id", "receiving_facility_type") - PartnerMetadata metadata2 = new PartnerMetadata("abcdefghi", null, sender, null, null, null, null, null, null, null, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") - PartnerMetadata metadata1 = new PartnerMetadata("123456789", null, sender, null, null, null, null, null, null, null, sendingApp, sendingFacility, receivingApp, receivingFacility, "placer_order_number") + PartnerMetadata metadata2 = new PartnerMetadata("abcdefghi", null, sender, null, null, null, null, null, null, null, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number") + PartnerMetadata metadata1 = new PartnerMetadata("123456789", null, sender, null, null, null, null, null, null, null, sendingAppDetails, sendingFacilityDetails, receivingAppDetails, receivingFacilityDetails, "placer_order_number") TestApplicationContext.register(Formatter, Jackson.getInstance()) TestApplicationContext.injectRegisteredImplementations() @@ -118,4 +106,52 @@ class FilePartnerMetadataStorageTest extends Specification { then: metadataSet.containsAll(Set.of(metadata1, metadata2)) } + + def "readMetadataForMessageLinking returns a set of PartnerMetadata"() { + given: + def receivedSubmissionId = "receivedSubmissionId" + def placerOrderNumber = "placerOrderNumber" + def sendingAppDetails = new MessageHdDataType("sending_app_name", "sending_app_id", "sending_app_type") + def sendingFacilityDetails = new MessageHdDataType("sending_facility_name", "sending_facility_id", "sending_facility_type") + PartnerMetadata metadata1 = new PartnerMetadata(receivedSubmissionId, null, null, null, null, null, null, null, null, null, sendingAppDetails, sendingFacilityDetails, null, null, placerOrderNumber) + PartnerMetadata metadata2 = new PartnerMetadata("2", null, null, null, null, null, null, null, null, null, sendingAppDetails, sendingFacilityDetails, null, null, placerOrderNumber) + + TestApplicationContext.register(Formatter, Jackson.getInstance()) + TestApplicationContext.injectRegisteredImplementations() + + when: + FilePartnerMetadataStorage.getInstance().saveMetadata(metadata1) + FilePartnerMetadataStorage.getInstance().saveMetadata(metadata2) + def metadataSet = FilePartnerMetadataStorage.getInstance().readMetadataForMessageLinking(receivedSubmissionId) + + then: + metadataSet.containsAll(Set.of(metadata1, metadata2)) + } + + def "readMetadataForMessageLinking returns an empty set when no metadata is found"() { + when: + def metadataSet = FilePartnerMetadataStorage.getInstance().readMetadataForMessageLinking("nonexistentId") + + then: + metadataSet.isEmpty() + } + + def "readMetadataForMessageLinking throws PartnerMetadataException when unable to parse file"() { + given: + def mockFormatter = Mock(Formatter) + mockFormatter.convertJsonToObject(_ as String, _ as TypeReference) >> {throw new FormatterProcessingException("error", new Exception())} + mockFormatter.convertToJsonString(_) >> "[]" + TestApplicationContext.register(Formatter, mockFormatter) + TestApplicationContext.injectRegisteredImplementations() + + def submissionId = "submissionId" + PartnerMetadata metadata = new PartnerMetadata(submissionId, null, null, null, null, null, null, null, null, null, null, null, null, null, null) + FilePartnerMetadataStorage.getInstance().saveMetadata(metadata) + + when: + FilePartnerMetadataStorage.getInstance().readMetadataForMessageLinking("submissionId") + + then: + thrown(PartnerMetadataException) + } }