diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000000..003aa9d99fa --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000000..6e6eec11483 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/prime-router/src/main/kotlin/azure/DatabaseAccess.kt b/prime-router/src/main/kotlin/azure/DatabaseAccess.kt index d72c1b4eba2..e293e6cdfa2 100644 --- a/prime-router/src/main/kotlin/azure/DatabaseAccess.kt +++ b/prime-router/src/main/kotlin/azure/DatabaseAccess.kt @@ -407,8 +407,8 @@ class DatabaseAccess(val create: DSLContext) : Logging { txn: DataAccessTransaction? = null, ): CovidResultMetadata? { val ctx = if (txn != null) DSL.using(txn) else create - return ctx.selectFrom(Tables.COVID_RESULT_METADATA) - .where(Tables.COVID_RESULT_METADATA.MESSAGE_ID.eq(messageID.toString())) + return ctx.selectFrom(COVID_RESULT_METADATA) + .where(COVID_RESULT_METADATA.MESSAGE_ID.eq(messageID.toString())) .fetchOne() ?.into(CovidResultMetadata::class.java) } @@ -476,11 +476,11 @@ class DatabaseAccess(val create: DSLContext) : Logging { ): List { val ctx = if (txn != null) DSL.using(txn) else create return ctx - .selectFrom(Tables.ACTION_LOG) + .selectFrom(ACTION_LOG) .where( - Tables.ACTION_LOG.REPORT_ID.eq(reportId) - .and(Tables.ACTION_LOG.TRACKING_ID.eq(trackingId)) - .and(Tables.ACTION_LOG.TYPE.eq(type)) + ACTION_LOG.REPORT_ID.eq(reportId) + .and(ACTION_LOG.TRACKING_ID.eq(trackingId)) + .and(ACTION_LOG.TYPE.eq(type)) ) .limit(100) .fetchInto(DetailedActionLog::class.java) @@ -1280,8 +1280,8 @@ class DatabaseAccess(val create: DSLContext) : Logging { // todo: migrate away from the covid test data which is legacy // we are going to have a more generic full elr table that we want to use // instead, but for now we need to maintain the older covid result metadata table - DatabaseAccess.saveTestData(actionHistory.elrMetaDataRecords, txn) - DatabaseAccess.saveCovidTestData(actionHistory.covidResultMetadataRecords, txn) + saveTestData(actionHistory.elrMetaDataRecords, txn) + saveCovidTestData(actionHistory.covidResultMetadataRecords, txn) // generate lineage records actionHistory.generateLineages() @@ -1347,8 +1347,8 @@ class DatabaseAccess(val create: DSLContext) : Logging { /** * Inserts the provided [actionLog] using [txn] as the data context. */ - private fun insertActionLog(actionLog: ActionLog, txn: Configuration) { - val detailRecord = DSL.using(txn).newRecord(Tables.ACTION_LOG, actionLog) + fun insertActionLog(actionLog: ActionLog, txn: Configuration) { + val detailRecord = DSL.using(txn).newRecord(ACTION_LOG, actionLog) detailRecord.store() } diff --git a/prime-router/src/main/kotlin/azure/ReportFunction.kt b/prime-router/src/main/kotlin/azure/ReportFunction.kt index 70fc5aabed4..24c535ed849 100644 --- a/prime-router/src/main/kotlin/azure/ReportFunction.kt +++ b/prime-router/src/main/kotlin/azure/ReportFunction.kt @@ -225,7 +225,9 @@ class ReportFunction( workflowEngine.recordAction(actionHistory) check(actionHistory.action.actionId > 0) - val submission = SubmissionsFacade.instance.findDetailedSubmissionHistory(actionHistory.action) + val submission = workflowEngine.db.transactReturning { txn -> + SubmissionsFacade.instance.findDetailedSubmissionHistory(txn, null, actionHistory.action) + } val response = request.createResponseBuilder(httpStatus) .header(HttpHeaders.CONTENT_TYPE, "application/json") @@ -236,7 +238,7 @@ class ReportFunction( .header( HttpHeaders.LOCATION, request.uri.resolve( - "/api/history/${sender.organizationName}/submissions/${actionHistory.action.actionId}" + "/api/waters/report/${submission?.reportId}/history" ).toString() ) .build() diff --git a/prime-router/src/main/kotlin/history/ReportHistory.kt b/prime-router/src/main/kotlin/history/ReportHistory.kt index cd5bc2c0d2f..4d187c5398a 100644 --- a/prime-router/src/main/kotlin/history/ReportHistory.kt +++ b/prime-router/src/main/kotlin/history/ReportHistory.kt @@ -11,6 +11,7 @@ import gov.cdc.prime.router.ActionLogScope import gov.cdc.prime.router.ErrorCode import gov.cdc.prime.router.ItemActionLogDetail import gov.cdc.prime.router.Topic +import gov.cdc.prime.router.azure.db.enums.TaskAction import java.time.OffsetDateTime import java.util.UUID @@ -77,6 +78,12 @@ data class DetailedReport( val itemCountBeforeQualFilter: Int?, @JsonIgnore val receiverHasTransport: Boolean, + @JsonIgnore + val transportResult: String?, + @JsonIgnore + val downloadedBy: String?, + @JsonIgnore + val nextAction: TaskAction?, ) /** diff --git a/prime-router/src/main/kotlin/history/SubmissionHistory.kt b/prime-router/src/main/kotlin/history/SubmissionHistory.kt index cc6f6134f37..15f7af3aaee 100644 --- a/prime-router/src/main/kotlin/history/SubmissionHistory.kt +++ b/prime-router/src/main/kotlin/history/SubmissionHistory.kt @@ -12,7 +12,6 @@ import gov.cdc.prime.router.ActionLogLevel import gov.cdc.prime.router.ActionLogScope import gov.cdc.prime.router.ClientSource import gov.cdc.prime.router.ReportStreamFilterResult -import gov.cdc.prime.router.Sender import gov.cdc.prime.router.Topic import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.common.BaseEngine @@ -120,9 +119,9 @@ class DetailedSubmissionHistory( createdAt: OffsetDateTime, httpStatus: Int? = null, @JsonIgnore - val reports: MutableList? = mutableListOf(), + val reports: List, @JsonIgnore - var logs: List = emptyList(), + var logs: List, ) : SubmissionHistory( actionId, createdAt, @@ -240,9 +239,13 @@ class DetailedSubmissionHistory( } init { - reports?.forEach { report -> - // For reports sent to a destination - report.receivingOrg?.let { + // Iterate over all the reports in the report lineage, processing them to generate the + // destinations + reports.forEach { report -> + + // If the report has a receiving org, it means that it contains information about a destination + report.receivingOrg?.apply { + val filterLogs = logs.filter { it.type == ActionLogLevel.filter && it.reportId == report.reportId } @@ -250,26 +253,40 @@ class DetailedSubmissionHistory( val filteredReportItems = filterLogs.map { ReportStreamFilterResultForResponse(it.detail as ReportStreamFilterResult) } - // If there is no transport defined, there will not be a next action so sending at - // should be null. - val nextActionTime = if (report.receiverHasTransport) report.nextActionAt else null - destinations.add( - Destination( - report.receivingOrg, - report.receivingOrgSvc!!, - filteredReportRows.toMutableList(), - filteredReportItems.toMutableList(), - nextActionTime, - report.itemCount, - report.itemCountBeforeQualFilter, + + val existingDestination = + destinations.find { + it.organizationId == report.receivingOrg && it.service == report.receivingOrgSvc + } + val sentReports = if (report.transportResult != null) mutableListOf(report) else mutableListOf() + val downloadedReports = if (report.downloadedBy != null) mutableListOf(report) else mutableListOf() + + if (existingDestination == null) { + + destinations.add( + Destination( + report.receivingOrg, + report.receivingOrgSvc!!, + filteredReportRows.toMutableList(), + filteredReportItems.toMutableList(), + report.nextActionAt, + report.itemCount, + report.itemCountBeforeQualFilter, + sentReports = sentReports, + downloadedReports + ) ) - ) + } else { + existingDestination.sentReports.addAll(sentReports) + existingDestination.downloadedReports.addAll(downloadedReports) + if (report.nextActionAt != null) { + existingDestination.sendingAt = report.nextActionAt + } + } } // For the report received from a sender if (report.sendingOrg != null) { - // There can only be one! - check(reportId == null) // Reports with errors do not show an ID reportId = if (errorCount == 0) report.reportId.toString() else null externalName = report.externalName @@ -277,10 +294,20 @@ class DetailedSubmissionHistory( sender = ClientSource(report.sendingOrg, report.sendingOrgClient ?: "").name topic = report.schemaTopic } - // if there is ANY action scheduled on this submission history, ensure this flag is true if (report.nextActionAt != null) nextActionScheduled = true } + destinations.forEach { destination -> + val reportsForDestination = reports.filter { + destination.organizationId == it.receivingOrg && destination.service == it.receivingOrgSvc + }.sortedBy { it.createdAt } + val latestAction = reportsForDestination.first().nextAction + val reportsGroupedByLatestAction = reportsForDestination.groupBy { it.nextAction } + val mostRecentReportsForDestination = reportsGroupedByLatestAction[latestAction] ?: emptyList() + destination.itemCount = mostRecentReportsForDestination.sumOf { it.itemCount } + destination.itemCountBeforeQualFilter = + mostRecentReportsForDestination.sumOf { it.itemCountBeforeQualFilter ?: 0 } + } errors.addAll(consolidateLogs(ActionLogLevel.error)) warnings.addAll(consolidateLogs(ActionLogLevel.warning)) } @@ -325,240 +352,6 @@ class DetailedSubmissionHistory( return consolidatedList } - // TODO: https://github.com/CDCgov/prime-reportstream/issues/14350 - /** - * Enrich the submission history with various other related bits of history. - * - * @param descendants[] the various bits of DetailedSubmissionHistory that will be used to enrich - */ - fun enrichWithDescendants(descendants: List) { - check(descendants.distinctBy { it.actionId }.size == descendants.size) - actionsPerformed.addAll(descendants.map { submission -> submission.actionName }.distinct()) - - // Enforce an order on the enrichment: process/translate, send, download - if (topic?.isUniversalPipeline == true) { - // logs and destinations are handled very differently for UP - // both routing and translate are populated at different times, - // so we need to do special logic to handle them - descendants.filter { it.actionName == TaskAction.translate }.forEach { descendant -> - enrichWithTranslateAction(descendant) - } - descendants.filter { it.actionName == TaskAction.route }.forEach { descendant -> - enrichWithRouteAction(descendant) - } - descendants.filter { it.actionName == TaskAction.convert }.forEach { descendant -> - enrichWithConvertAction(descendant) - } - } else { - descendants.filter { - it.actionName == TaskAction.process - }.forEach { descendant -> - enrichWithProcessAction(descendant) - } - } - - // note: we do not use any data from the batch action at this time. - descendants.filter { it.actionName == TaskAction.send }.forEach { descendant -> - enrichWithSendAction(descendant) - } - descendants.filter { it.actionName == TaskAction.download }.forEach { descendant -> - enrichWithDownloadAction(descendant) - } - } - - /** - * Enrich a parent detailed history with details from translate actions. - * Add destinations, errors, and warnings, to the history details. - * Note: Route/Translate is exclusive to the Universal pipeline - * See enrichWithProcessAction for the TopicReceiver pipeline counterpart - * - * @param descendant translate action that will be used to enrich - */ - private fun enrichWithTranslateAction(descendant: DetailedSubmissionHistory) { - require( - topic?.isUniversalPipeline == true && - descendant.actionName == TaskAction.translate - ) { - "Must be translate action. Enrichment is only available for the Universal Pipeline" - } - - // Grab destinations from the "translate" action - descendant.destinations.forEach { descendantDest -> - // Check if destination has already been added - // if it is increase item counts - // otherwise add it to destinations - destinations.firstOrNull { - it.organizationId == descendantDest.organizationId && it.service == descendantDest.service - }?.let { existingDestination -> - existingDestination.itemCount += descendantDest.itemCount - existingDestination.itemCountBeforeQualFilter = - existingDestination.itemCountBeforeQualFilter?.plus( - descendantDest.itemCountBeforeQualFilter ?: 0 - ) ?: descendantDest.itemCountBeforeQualFilter - } ?: run { - destinations += descendantDest - } - } - } - - /** - * Enrich a parent detailed history with details from the convert action. - * Add errors, and warnings, to the history details. - * - * @param descendant route action that will be used to enrich - */ - private fun enrichWithConvertAction(descendant: DetailedSubmissionHistory) { - require( - topic?.isUniversalPipeline == true && - descendant.actionName == TaskAction.convert - ) { - "Must be route action. Enrichment is only available for the Universal Pipeline" - } - errors += descendant.errors - warnings += descendant.warnings - } - - /** - * Enrich a parent detailed history with details from the route action. - * Add destinations, errors, and warnings, to the history details. - * Note: Route/Translate is exclusive to the Universal pipeline - * See enrichWithProcessAction for the TopicReceiver pipeline counterpart - * - * @param descendant route action that will be used to enrich - */ - private fun enrichWithRouteAction(descendant: DetailedSubmissionHistory) { - require( - topic?.isUniversalPipeline == true && - descendant.actionName == TaskAction.route - ) { - "Must be route action. Enrichment is only available for the Universal Pipeline" - } - // Grab the filter logs generated during the "route" action, as well as errors and warnings - val filterLogs = descendant.logs.filter { log -> log.type == ActionLogLevel.filter } - errors += descendant.errors - warnings += descendant.warnings - - // add filter logs to its respective destination otherwise add new destination - if (filterLogs.isNotEmpty()) { - filterLogs.forEach { log -> - check(log.detail is ReportStreamFilterResult) { "Filter result not of type ReportStreamFilterResult" } - val filterResult = log.detail - val filterReport = log.detail.message - val receiverNameSegments = filterResult.receiverName.split(Sender.fullNameSeparator) - val filterResultResponse = ReportStreamFilterResultForResponse(filterResult) - - destinations.firstOrNull { - it.organizationId == receiverNameSegments[0] && it.service == receiverNameSegments[1] - }?.let { existingDestination -> - // filteredReportRows and filteredReportItems are initialized - // when a DetailedSubmissionHistory is created, so they shouldn't be null - existingDestination.filteredReportRows!!.add(filterReport) - existingDestination.filteredReportItems!!.add(filterResultResponse) - existingDestination.itemCountBeforeQualFilter = existingDestination.itemCountBeforeQualFilter?.plus( - filterResult.originalCount - ) ?: filterResult.originalCount - } ?: run { - destinations.add( - Destination( - receiverNameSegments[0], - receiverNameSegments[1], - mutableListOf(filterReport), - mutableListOf(ReportStreamFilterResultForResponse(filterResult)), - null, - 0, - filterResult.originalCount, - ) - ) - } - } - } - } - - /** - * Enrich a parent detailed history with details from the process action. - * Add destinations, errors, and warnings, to the history details. - * Note: Process is exclusive to the COVID pipeline - * See enrichWithRoutingAndTranslationActions for the Universal pipeline counterpart - * - * @param descendant the history used for enriching - */ - private fun enrichWithProcessAction(descendant: DetailedSubmissionHistory) { - require(descendant.actionName == TaskAction.process) { - "Must be a process action" - } - destinations += descendant.destinations - errors += descendant.errors - warnings += descendant.warnings - } - - /** - * Enrich a parent detailed history with details from the send action. - * Add sent report information to each destination present in the parent's historical details. - * - * @param descendant the history used for enriching - */ - private fun enrichWithSendAction(descendant: DetailedSubmissionHistory) { - require(descendant.actionName == TaskAction.send) { "Must be a send action" } - descendant.reports?.let { it -> - it.forEach { report -> - destinations.find { - it.organizationId == report.receivingOrg && it.service == report.receivingOrgSvc - }?.let { - it.sentReports.add(report) - } ?: run { - if (report.receivingOrg != null && report.receivingOrgSvc != null) { - destinations.add( - Destination( - report.receivingOrg, - report.receivingOrgSvc, - null, - null, - null, - report.itemCount, - report.itemCountBeforeQualFilter, - ) - ) - } - } - } - } - } - - /** - * Enrich a parent detailed history with details from the download action - * Add download report information to each destination present in the parent's historical details. - * - * @param descendant the history used for enriching - */ - private fun enrichWithDownloadAction(descendant: DetailedSubmissionHistory) { - require(descendant.actionName == TaskAction.download) { "Must be a download action" } - - descendant.reports?.let { it -> - it.forEach { report -> - destinations.find { - it.organizationId == report.receivingOrg && it.service == report.receivingOrgSvc - }?.let { - it.downloadedReports.add(report) - } ?: run { - if (report.receivingOrg != null && report.receivingOrgSvc != null) { - val dest = Destination( - report.receivingOrg, - report.receivingOrgSvc, - null, - null, - null, - report.itemCount, - report.itemCountBeforeQualFilter, - ) - - destinations.add(dest) - dest.downloadedReports.add(report) - } - } - } - } - } - /** * Update the summary fields for this Submission report based on the destinations that * will be receiving reports. @@ -591,8 +384,7 @@ class DetailedSubmissionHistory( * the receivers. */ return if ( - (actionsPerformed.contains(TaskAction.route) || actionsPerformed.contains(TaskAction.convert)) && - !nextActionScheduled + reports.size > 1 ) { Status.NOT_DELIVERING } else { @@ -703,7 +495,7 @@ data class Destination( val filteredReportItems: MutableList?, @JsonProperty("sending_at") @JsonInclude(Include.NON_NULL) - val sendingAt: OffsetDateTime?, + var sendingAt: OffsetDateTime?, var itemCount: Int, @JsonProperty("itemCountBeforeQualityFiltering") var itemCountBeforeQualFilter: Int?, diff --git a/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt b/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt index a543668519c..b63ab240924 100644 --- a/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt +++ b/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt @@ -12,6 +12,7 @@ import com.microsoft.azure.functions.annotation.HttpTrigger import gov.cdc.prime.router.CustomerStatus import gov.cdc.prime.router.Sender import gov.cdc.prime.router.azure.ApiResponse +import gov.cdc.prime.router.azure.DataAccessTransaction import gov.cdc.prime.router.azure.HttpUtilities import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.enums.TaskAction @@ -165,7 +166,7 @@ class DeliveryFunction( * @param action Action from which the data for the delivery is loaded * @return */ - override fun singleDetailedHistory(queryParams: MutableMap, action: Action): DeliveryHistory? { + override fun singleDetailedHistory(id: String, txn: DataAccessTransaction, action: Action): DeliveryHistory? { return deliveryFacade.findDetailedDeliveryHistory(action.actionId) } diff --git a/prime-router/src/main/kotlin/history/azure/ReportFileFacade.kt b/prime-router/src/main/kotlin/history/azure/ReportFileFacade.kt index b3cef09c691..e95a6e2319d 100644 --- a/prime-router/src/main/kotlin/history/azure/ReportFileFacade.kt +++ b/prime-router/src/main/kotlin/history/azure/ReportFileFacade.kt @@ -1,6 +1,7 @@ package gov.cdc.prime.router.history.azure import com.microsoft.azure.functions.HttpRequestMessage +import gov.cdc.prime.router.azure.DataAccessTransaction import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.db.tables.pojos.Action import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile @@ -26,8 +27,8 @@ abstract class ReportFileFacade( /** * @return a single Report associated with this [actionId] */ - fun fetchReportForActionId(actionId: Long): ReportFile? { - return dbAccess.fetchReportForActionId(actionId) + fun fetchReportForActionId(actionId: Long, txn: DataAccessTransaction? = null): ReportFile? { + return dbAccess.fetchReportForActionId(actionId, txn) } /** diff --git a/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt b/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt index 35e978f6ffc..cf88c750881 100644 --- a/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt +++ b/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt @@ -6,6 +6,7 @@ import com.microsoft.azure.functions.HttpResponseMessage import com.microsoft.azure.functions.HttpStatus import gov.cdc.prime.router.CustomerStatus import gov.cdc.prime.router.RESTTransportType +import gov.cdc.prime.router.azure.DataAccessTransaction import gov.cdc.prime.router.azure.HttpUtilities import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.tables.pojos.Action @@ -75,7 +76,7 @@ abstract class ReportFileFunction( * @param action Action from which the data for the report is loaded * @return */ - abstract fun singleDetailedHistory(queryParams: MutableMap, action: Action): ReportHistory? + abstract fun singleDetailedHistory(id: String, txn: DataAccessTransaction, action: Action): ReportHistory? /** * Verify that the action being checked has the correct data/parameters @@ -152,13 +153,10 @@ abstract class ReportFileFunction( authResult } else { val action = this.actionFromId(id) - val history = this.singleDetailedHistory(request.queryParameters, action) - - if (history != null) { - HttpUtilities.okJSONResponse(request, history) - } else { - HttpUtilities.notFoundResponse(request, "History entry ${action.actionId} was not found.") + val history = workflowEngine.db.transactReturning { txn -> + this.singleDetailedHistory(id, txn, action) } + HttpUtilities.okJSONResponse(request, history) } } catch (e: DataAccessException) { logger.error("Unable to fetch history for ID $id", e) diff --git a/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt b/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt index 7fd7c8687ca..c63d1dcdb18 100644 --- a/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt +++ b/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt @@ -9,6 +9,7 @@ import com.microsoft.azure.functions.annotation.BindingName import com.microsoft.azure.functions.annotation.FunctionName import com.microsoft.azure.functions.annotation.HttpTrigger import gov.cdc.prime.router.Sender +import gov.cdc.prime.router.azure.DataAccessTransaction import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.Action @@ -91,10 +92,20 @@ class SubmissionFunction( * @return */ override fun singleDetailedHistory( - queryParams: MutableMap, + id: String, + txn: DataAccessTransaction, action: Action, ): DetailedSubmissionHistory? { - return submissionsFacade.findDetailedSubmissionHistory(action) + val report = try { + val reportId = UUID.fromString(id) + submissionsFacade.findDetailedSubmissionHistory(txn, reportId, action) + } catch (ex: IllegalArgumentException) { + // We cannot consistently use this logic because the covid pipeline can process reports + // synchronously such that the intial action has multiple reports associated (i.e. receive and send) + val report = submissionsFacade.fetchReportForActionId(action.actionId, txn) + submissionsFacade.findDetailedSubmissionHistory(txn, report?.reportId, action) + } + return report } /** diff --git a/prime-router/src/main/kotlin/history/azure/SubmissionsFacade.kt b/prime-router/src/main/kotlin/history/azure/SubmissionsFacade.kt index 116754549bd..d83a1bd7918 100644 --- a/prime-router/src/main/kotlin/history/azure/SubmissionsFacade.kt +++ b/prime-router/src/main/kotlin/history/azure/SubmissionsFacade.kt @@ -1,14 +1,19 @@ package gov.cdc.prime.router.history.azure import com.microsoft.azure.functions.HttpRequestMessage +import gov.cdc.prime.router.azure.DataAccessTransaction import gov.cdc.prime.router.azure.DatabaseAccess -import gov.cdc.prime.router.azure.db.enums.TaskAction +import gov.cdc.prime.router.azure.db.Tables import gov.cdc.prime.router.azure.db.tables.pojos.Action import gov.cdc.prime.router.common.BaseEngine import gov.cdc.prime.router.common.JacksonMapperUtilities +import gov.cdc.prime.router.history.DetailedActionLog +import gov.cdc.prime.router.history.DetailedReport import gov.cdc.prime.router.history.DetailedSubmissionHistory import gov.cdc.prime.router.history.SubmissionHistory +import gov.cdc.prime.router.history.db.ReportGraph import gov.cdc.prime.router.tokens.AuthenticatedClaims +import org.jooq.impl.DSL import java.time.OffsetDateTime import java.util.UUID @@ -18,7 +23,8 @@ import java.util.UUID */ class SubmissionsFacade( private val dbSubmissionAccess: HistoryDatabaseAccess = DatabaseSubmissionsAccess(), - dbAccess: DatabaseAccess = BaseEngine.databaseAccessSingleton, + val dbAccess: DatabaseAccess = BaseEngine.databaseAccessSingleton, + private val reportGraph: ReportGraph = ReportGraph(), ) : ReportFileFacade( dbAccess, ) { @@ -116,30 +122,56 @@ class SubmissionsFacade( * @return Report details */ fun findDetailedSubmissionHistory( + txn: DataAccessTransaction, + reportId: UUID?, action: Action, ): DetailedSubmissionHistory? { - // This assumes that ReportFileFunction.authSingleBlocks has already run, and has checked that the - // sendingOrg is good. If that assumption is incorrect, die here. - assert(action.sendingOrg != null && action.actionName == TaskAction.receive) - val submission = dbSubmissionAccess.fetchAction( - action.actionId, - action.sendingOrg, - DetailedSubmissionHistory::class.java - ) - submission?.actionsPerformed?.add(action.actionName) - - // Submissions with a report ID (means had no errors) can have a lineage - submission?.reportId?.let { - val relatedSubmissions = dbSubmissionAccess.fetchRelatedActions( - UUID.fromString(it), + if (reportId == null) { + return dbSubmissionAccess.fetchAction( + action.actionId, + action.sendingOrg, DetailedSubmissionHistory::class.java ) - submission.enrichWithDescendants(relatedSubmissions) } + val graph = reportGraph.getDescendantReports(txn, reportId) + val detailedReports = graph.map { reportFile -> + DetailedReport( + reportFile.reportId, + reportFile.receivingOrg, + reportFile.receivingOrgSvc, + reportFile.sendingOrg, + reportFile.sendingOrgClient, + reportFile.schemaTopic, + reportFile.externalName, + reportFile.createdAt, + reportFile.nextActionAt, + reportFile.itemCount, + reportFile.itemCountBeforeQualFilter, + reportFile.transportResult != null, + reportFile.transportResult, + reportFile.downloadedBy, + reportFile.nextAction + ) + }.toMutableList() + val reportIds = graph.map { it.reportId } + val logs = DSL + .using(txn) + .select() + .from(Tables.ACTION_LOG) + .where(Tables.ACTION_LOG.REPORT_ID.`in`(reportIds)) + .fetchInto(DetailedActionLog::class.java) + val history = + DetailedSubmissionHistory( + action.actionId, + action.actionName, + action.createdAt, + httpStatus = action.httpStatus, + logs = logs, + reports = detailedReports - submission?.enrichWithSummary() - - return submission + ) + history.enrichWithSummary() + return history } /** diff --git a/prime-router/src/main/kotlin/history/db/ReportGraph.kt b/prime-router/src/main/kotlin/history/db/ReportGraph.kt index afddbb1d341..910b837af1b 100644 --- a/prime-router/src/main/kotlin/history/db/ReportGraph.kt +++ b/prime-router/src/main/kotlin/history/db/ReportGraph.kt @@ -17,8 +17,10 @@ import gov.cdc.prime.router.common.BaseEngine import org.apache.logging.log4j.kotlin.Logging import org.jooq.CommonTableExpression import org.jooq.DSLContext +import org.jooq.Record import org.jooq.Record1 import org.jooq.Record2 +import org.jooq.SelectOnConditionStep import org.jooq.impl.CustomRecord import org.jooq.impl.CustomTable import org.jooq.impl.DSL @@ -157,7 +159,7 @@ class ReportGraph( fun getDescendantReports( txn: DataAccessTransaction, parentReportId: UUID, - searchedForTaskActions: Set, + searchedForTaskActions: Set? = null, ): List { val cte = reportDescendantGraphCommonTableExpression(listOf(parentReportId)) return descendantReportRecords(txn, cte, searchedForTaskActions).fetchInto(ReportFile::class.java) @@ -408,15 +410,22 @@ class ReportGraph( private fun descendantReportRecords( txn: DataAccessTransaction, cte: CommonTableExpression>, - searchedForTaskActions: Set, - ) = DSL.using(txn) - .withRecursive(cte) - .select(REPORT_FILE.asterisk()) - .distinctOn(REPORT_FILE.REPORT_ID) - .from(cte) - .join(REPORT_FILE) - .on(REPORT_FILE.REPORT_ID.eq(cte.field(0, UUID::class.java))) - .join(ACTION) - .on(ACTION.ACTION_ID.eq(REPORT_FILE.ACTION_ID)) - .where(ACTION.ACTION_NAME.`in`(searchedForTaskActions)) + searchedForTaskActions: Set?, + ): SelectOnConditionStep { + val select = DSL.using(txn) + .withRecursive(cte) + .select(REPORT_FILE.asterisk()) + .distinctOn(REPORT_FILE.REPORT_ID) + .from(cte) + .join(REPORT_FILE) + .on(REPORT_FILE.REPORT_ID.eq(cte.field(0, UUID::class.java))) + .join(ACTION) + .on(ACTION.ACTION_ID.eq(REPORT_FILE.ACTION_ID)) + + if (searchedForTaskActions != null) { + select.where(ACTION.ACTION_NAME.`in`(searchedForTaskActions)) + } + + return select + } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/azure/HttpTestUtils.kt b/prime-router/src/test/kotlin/azure/HttpTestUtils.kt index 8b4604943f2..b171a3763bb 100644 --- a/prime-router/src/test/kotlin/azure/HttpTestUtils.kt +++ b/prime-router/src/test/kotlin/azure/HttpTestUtils.kt @@ -15,13 +15,14 @@ import kotlin.collections.Map class MockHttpResponseMessage : HttpResponseMessage.Builder, HttpResponseMessage { var httpStatus: HttpStatusType = HttpStatus.OK var content: Any? = null + var headers: MutableMap = mutableMapOf() override fun getStatus(): HttpStatusType { return this.httpStatus } override fun getHeader(var1: String): String { - return "world" + return headers.getOrDefault(var1, "world") } override fun getBody(): Any? { @@ -34,6 +35,7 @@ class MockHttpResponseMessage : HttpResponseMessage.Builder, HttpResponseMessage } override fun header(key: String, value: String): HttpResponseMessage.Builder { + headers[key] = value return this } diff --git a/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt b/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt index e37a252cd38..e08577b2ad4 100644 --- a/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt +++ b/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt @@ -1,5 +1,8 @@ package gov.cdc.prime.router.azure +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.google.common.net.HttpHeaders import com.microsoft.azure.functions.HttpStatus import gov.cdc.prime.router.ActionLog import gov.cdc.prime.router.ClientSource @@ -21,12 +24,15 @@ import gov.cdc.prime.router.Topic import gov.cdc.prime.router.TopicReceiver import gov.cdc.prime.router.UniversalPipelineSender import gov.cdc.prime.router.azure.db.enums.TaskAction +import gov.cdc.prime.router.history.DetailedSubmissionHistory +import gov.cdc.prime.router.history.azure.SubmissionsFacade import gov.cdc.prime.router.serializers.Hl7Serializer import gov.cdc.prime.router.tokens.AuthenticatedClaims import gov.cdc.prime.router.tokens.AuthenticationType import gov.cdc.prime.router.unittest.UnitTestUtils import io.mockk.clearAllMocks import io.mockk.every +import io.mockk.mockk import io.mockk.mockkClass import io.mockk.mockkObject import io.mockk.spyk @@ -37,6 +43,8 @@ import org.jooq.tools.jdbc.MockResult import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import java.time.OffsetDateTime +import java.util.UUID class ReportFunctionTests { val dataProvider = MockDataProvider { emptyArray() } @@ -585,6 +593,20 @@ class ReportFunctionTests { every { accessSpy.isDuplicateItem(any(), any()) } returns false + val mockSubmissionsFacade = mockk() + mockkObject(SubmissionsFacade) + every { SubmissionsFacade.instance } returns mockSubmissionsFacade + val reportId = UUID.randomUUID() + val mockHistory = DetailedSubmissionHistory( + 1, + TaskAction.receive, + OffsetDateTime.now(), + reports = mutableListOf(), + logs = emptyList() + ) + mockHistory.reportId = reportId.toString() + every { mockSubmissionsFacade.findDetailedSubmissionHistory(any(), any(), any()) } returns mockHistory + // act val resp = reportFunc.processRequest(req, sender) @@ -592,6 +614,8 @@ class ReportFunctionTests { verify(exactly = 1) { engine.isDuplicateItem(any()) } verify(exactly = 1) { actionHistory.trackActionSenderInfo(any(), any()) } assert(resp.status.equals(HttpStatus.CREATED)) + assertThat(resp.getHeader(HttpHeaders.LOCATION)) + .isEqualTo("http://localhost/api/waters/report/$reportId/history") } // test processFunction when basic hl7 message with 5 separators is passed diff --git a/prime-router/src/test/kotlin/history/SubmissionHistoryTests.kt b/prime-router/src/test/kotlin/history/SubmissionHistoryTests.kt index d7d2a230753..151f48b9b88 100644 --- a/prime-router/src/test/kotlin/history/SubmissionHistoryTests.kt +++ b/prime-router/src/test/kotlin/history/SubmissionHistoryTests.kt @@ -1,8 +1,6 @@ package gov.cdc.prime.router.history -import assertk.assertFailure import assertk.assertThat -import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse @@ -14,7 +12,6 @@ import com.microsoft.azure.functions.HttpStatus import gov.cdc.prime.router.ActionLogLevel import gov.cdc.prime.router.ActionLogScope import gov.cdc.prime.router.ClientSource -import gov.cdc.prime.router.ErrorCode import gov.cdc.prime.router.FieldPrecisionMessage import gov.cdc.prime.router.InvalidEquipmentMessage import gov.cdc.prime.router.InvalidReportMessage @@ -24,7 +21,6 @@ import gov.cdc.prime.router.ReportStreamFilterResult import gov.cdc.prime.router.ReportStreamFilterType import gov.cdc.prime.router.Topic import gov.cdc.prime.router.azure.db.enums.TaskAction -import gov.cdc.prime.router.fhirengine.engine.FHIRConverter import java.time.OffsetDateTime import java.util.UUID import kotlin.test.Test @@ -35,7 +31,7 @@ class SubmissionHistoryTests { fun createLogs(logs: List): DetailedSubmissionHistory { return DetailedSubmissionHistory( 1, TaskAction.receive, OffsetDateTime.now(), - null, null, logs + null, mutableListOf(), logs ) } @@ -369,6 +365,8 @@ class SubmissionHistoryTests { 1, TaskAction.receive, OffsetDateTime.now(), + reports = mutableListOf(), + logs = emptyList() ).run { assertThat(actionId).isEqualTo(1) assertThat(createdAt).isNotNull() @@ -380,7 +378,7 @@ class SubmissionHistoryTests { assertThat(externalName).isEqualTo("") assertThat(destinations.size).isEqualTo(0) assertThat(destinationCount).isEqualTo(0) - assertThat(reports?.size).isEqualTo(0) + assertThat(reports.size).isEqualTo(0) assertThat(logs.size).isEqualTo(0) } DetailedSubmissionHistory( @@ -388,7 +386,7 @@ class SubmissionHistoryTests { TaskAction.receive, OffsetDateTime.now(), 201, - null, + mutableListOf(), emptyList(), ).run { assertThat(actionId).isEqualTo(1) @@ -401,7 +399,7 @@ class SubmissionHistoryTests { assertThat(externalName).isEqualTo("") assertThat(destinations.size).isEqualTo(0) assertThat(destinationCount).isEqualTo(0) - assertThat(reports).isNull() + assertThat(reports).isEmpty() assertThat(logs.size).isEqualTo(0) } @@ -410,7 +408,7 @@ class SubmissionHistoryTests { TaskAction.receive, OffsetDateTime.now(), null, - null, + mutableListOf(), emptyList(), ).run { assertThat(actionId).isEqualTo(1) @@ -438,7 +436,10 @@ class SubmissionHistoryTests { null, 5, 7, - false + false, + null, + null, + null ) val refUUID = UUID.randomUUID() @@ -456,7 +457,10 @@ class SubmissionHistoryTests { null, 1, 1, - true + true, + null, + null, + null ), DetailedReport( UUID.randomUUID(), @@ -470,7 +474,10 @@ class SubmissionHistoryTests { null, 2, null, - true + true, + null, + null, + null ), DetailedReport( UUID.randomUUID(), @@ -483,7 +490,10 @@ class SubmissionHistoryTests { null, 0, null, - true + true, + null, + null, + null ), ).toMutableList() @@ -549,19 +559,6 @@ class SubmissionHistoryTests { assertThat(logs).isNotNull() } - - reports = listOf(inputReport, inputReport).toMutableList() - - assertFailure { - DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - null, - reports, - emptyList() - ) - } } // Status calculation tests @@ -575,6 +572,8 @@ class SubmissionHistoryTests { TaskAction.receive, OffsetDateTime.now(), HttpStatus.BAD_REQUEST.value(), + reports = mutableListOf(), + logs = emptyList() ) testError.enrichWithSummary() testError.run { @@ -592,6 +591,8 @@ class SubmissionHistoryTests { TaskAction.receive, OffsetDateTime.now(), HttpStatus.OK.value(), + reports = mutableListOf(), + logs = emptyList() ) testReceived.actionsPerformed = mutableSetOf(TaskAction.receive) testReceived.enrichWithSummary() @@ -602,7 +603,7 @@ class SubmissionHistoryTests { val noDestinationsCalculatedYet = emptyList().toMutableList() val testReceivedButNoDestinationsYet = DetailedSubmissionHistory( 1, TaskAction.route, OffsetDateTime.now(), - HttpStatus.OK.value(), noDestinationsCalculatedYet + HttpStatus.OK.value(), noDestinationsCalculatedYet, logs = emptyList() ) testReceivedButNoDestinationsYet.enrichWithSummary() testReceivedButNoDestinationsYet.run { @@ -621,7 +622,10 @@ class SubmissionHistoryTests { null, 5, null, - false + false, + null, + null, + null ) // received: one of two destinations has been calculated, with all items for it filtered out val oneFilteredDestinationCalculated = listOf( @@ -638,12 +642,15 @@ class SubmissionHistoryTests { OffsetDateTime.now().plusDays(1), 0, 5, - true + true, + null, + null, + null ), ).toMutableList() val testReceivedOneFilteredDestination = DetailedSubmissionHistory( 1, TaskAction.route, OffsetDateTime.now(), - HttpStatus.OK.value(), oneFilteredDestinationCalculated + HttpStatus.OK.value(), oneFilteredDestinationCalculated, logs = emptyList() ) testReceivedOneFilteredDestination.enrichWithSummary() testReceivedOneFilteredDestination.run { @@ -664,7 +671,10 @@ class SubmissionHistoryTests { null, 0, null, - true + true, + null, + null, + null ), ).toMutableList() val testReceivedNoDestination = DetailedSubmissionHistory( @@ -673,6 +683,7 @@ class SubmissionHistoryTests { OffsetDateTime.now(), HttpStatus.OK.value(), reports, + emptyList() ) testReceivedNoDestination.enrichWithSummary() testReceivedNoDestination.run { @@ -688,7 +699,8 @@ class SubmissionHistoryTests { TaskAction.receive, OffsetDateTime.now(), HttpStatus.OK.value(), - null, + reports = mutableListOf(), + logs = emptyList() ) testReceived.actionsPerformed = mutableSetOf(TaskAction.receive) testReceived.enrichWithSummary() @@ -711,7 +723,10 @@ class SubmissionHistoryTests { null, 0, null, - true + true, + null, + null, + null ), ).toMutableList() val testReceivedNoDestination = DetailedSubmissionHistory( @@ -720,6 +735,7 @@ class SubmissionHistoryTests { OffsetDateTime.now(), HttpStatus.OK.value(), reports, + logs = emptyList() ) testReceivedNoDestination.enrichWithSummary() testReceivedNoDestination.run { @@ -744,7 +760,10 @@ class SubmissionHistoryTests { null, 5, null, - false + false, + null, + null, + null ) val latestReport = DetailedReport( UUID.randomUUID(), @@ -757,7 +776,10 @@ class SubmissionHistoryTests { null, 4, null, - true + true, + null, + null, + null ) // waiting to deliver: one of two destinations has been calculated, with no items filtered out val oneUnfilteredDestinationCalculated = listOf( @@ -774,12 +796,15 @@ class SubmissionHistoryTests { null, 5, 5, - true + true, + null, + null, + null ), ).toMutableList() val testReceivedOneUnfilteredDestination = DetailedSubmissionHistory( 1, TaskAction.route, OffsetDateTime.now(), - HttpStatus.OK.value(), oneUnfilteredDestinationCalculated + HttpStatus.OK.value(), oneUnfilteredDestinationCalculated, logs = emptyList() ) testReceivedOneUnfilteredDestination.enrichWithSummary() testReceivedOneUnfilteredDestination.run { @@ -801,7 +826,10 @@ class SubmissionHistoryTests { null, 1, null, - true + true, + null, + null, + null ), DetailedReport( UUID.randomUUID(), @@ -815,972 +843,19 @@ class SubmissionHistoryTests { null, 0, null, - true + true, + null, + null, + null ), ).toMutableList() val testWaitingToDeliver = DetailedSubmissionHistory( 1, TaskAction.receive, OffsetDateTime.now(), - HttpStatus.OK.value(), reports + HttpStatus.OK.value(), reports, logs = emptyList() ) testWaitingToDeliver.enrichWithSummary() testWaitingToDeliver.run { assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.WAITING_TO_DELIVER) } } - - @Test - fun `test DetailedSubmissionHistory overallStatus (partially delivered)`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - null, - false - ) - // use cases found while investigating issue #9378 - // partially delivered: one destination with an item, the other got all filtered out - val twoDestinationsOneItem = listOf( - inputReport, - DetailedReport( - UUID.randomUUID(), - "recvOrg4", - "recvSvc4", - null, - null, - Topic.FULL_ELR, - "one item dest", - null, - null, - 1, - 3, - true - ), - DetailedReport( - UUID.randomUUID(), - "recvOrg3", - "recvSvc3", - null, - null, - Topic.FULL_ELR, - "all items filtered out", - null, - null, - 0, - 3, - true - ), - ).toMutableList() - val testPartiallyDeliveredTwoDestinations = DetailedSubmissionHistory( - 1, TaskAction.route, OffsetDateTime.now(), - HttpStatus.OK.value(), twoDestinationsOneItem - ) - testPartiallyDeliveredTwoDestinations.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 1, TaskAction.send, OffsetDateTime.now(), - HttpStatus.OK.value(), twoDestinationsOneItem - ), - ) - ) - testPartiallyDeliveredTwoDestinations.enrichWithSummary() - testPartiallyDeliveredTwoDestinations.run { - assertThat(destinations.count()).isEqualTo(2) - assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.PARTIALLY_DELIVERED) - } - } - - @Test - fun `test DetailedSubmissionHistory overallStatus (delivered)`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - null, - false - ) - // delivered: one destination with an item, the other got SOME items filtered out - val twoDestinationsSomeItems = listOf( - inputReport, - DetailedReport( - UUID.randomUUID(), - "recvOrg4", - "recvSvc4", - null, - null, - Topic.FULL_ELR, - "one item dest", - null, - null, - 1, - 1, - true - ), - DetailedReport( - UUID.randomUUID(), - "recvOrg3", - "recvSvc3", - null, - null, - Topic.FULL_ELR, - "all items filtered out", - null, - null, - 1, - 4, - true - ), - ).toMutableList() - val testDeliveredTwoDestinationsSomeItems = DetailedSubmissionHistory( - 1, TaskAction.route, OffsetDateTime.now(), - HttpStatus.OK.value(), twoDestinationsSomeItems - ) - testDeliveredTwoDestinationsSomeItems.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 1, TaskAction.send, OffsetDateTime.now(), - HttpStatus.OK.value(), twoDestinationsSomeItems - ), - ) - ) - testDeliveredTwoDestinationsSomeItems.enrichWithSummary() - testDeliveredTwoDestinationsSomeItems.run { - assertThat(destinations.count()).isEqualTo(2) - assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.DELIVERED) - } - // delivered: all destinations received all items - val everyDestinationGetsAllItems = listOf( - inputReport, - DetailedReport( - UUID.randomUUID(), - "recvOrg4", - "recvSvc4", - null, - null, - Topic.FULL_ELR, - "one item dest", - null, - null, - 4, - 4, - true - ), - DetailedReport( - UUID.randomUUID(), - "recvOrg3", - "recvSvc3", - null, - null, - Topic.FULL_ELR, - "all items filtered out", - null, - null, - 3, - 3, - true - ), - ).toMutableList() - val testDeliveredToAllDestinations = DetailedSubmissionHistory( - 1, TaskAction.route, OffsetDateTime.now(), - HttpStatus.OK.value(), everyDestinationGetsAllItems - ) - testDeliveredToAllDestinations.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 1, TaskAction.send, OffsetDateTime.now(), - HttpStatus.OK.value(), everyDestinationGetsAllItems - ), - DetailedSubmissionHistory( - 2, TaskAction.send, OffsetDateTime.now(), - HttpStatus.OK.value(), everyDestinationGetsAllItems - ), - ) - ) - testDeliveredToAllDestinations.enrichWithSummary() - testDeliveredToAllDestinations.run { - assertThat(destinations.count()).isEqualTo(2) - assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.DELIVERED) - } - } - - @Test - fun `test DetailedSubmissionHistory UP overallStatus (not delivering)`() { - // not delivering: one destination, all items filtered out - val reportsAllItemsFilteredOut = listOf( - DetailedReport( - UUID.randomUUID(), - "recvOrg3", - "recvSvc3", - null, - null, - Topic.FULL_ELR, - "no item count dest", - null, - null, - 0, - 5, - true - ), - ).toMutableList() - val testNotDelivering = DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reportsAllItemsFilteredOut, - ) - testNotDelivering.enrichWithSummary() - testNotDelivering.run { - assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.NOT_DELIVERING) - } - val reports = listOf( - DetailedReport( - UUID.randomUUID(), - null, - null, - null, - null, - Topic.FULL_ELR, - "no item count dest", - null, - null, - 0, - null, - true - ), - ).toMutableList() - val testNotDeliveringNoDestination = DetailedSubmissionHistory( - 1, - TaskAction.route, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reports, - ) - testNotDeliveringNoDestination.actionsPerformed = mutableSetOf(TaskAction.route) - testNotDeliveringNoDestination.enrichWithSummary() - testNotDeliveringNoDestination.run { - assertThat(destinationCount).isEqualTo(0) - assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.NOT_DELIVERING) - } - } - - @Test - fun `test DetailedSubmissionHistory LEGACY overallStatus calculations (not delivering)`() { - var reports = listOf( - DetailedReport( - UUID.randomUUID(), - "recvOrg3", - "recvSvc3", - null, - null, - Topic.TEST, - "no item count dest", - null, - null, - 0, - null, - true - ), - ).toMutableList() - - val testNotDelivering = DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reports, - ) - testNotDelivering.enrichWithSummary() - testNotDelivering.run { - assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.NOT_DELIVERING) - assertThat(plannedCompletionAt).isNull() - assertThat(actualCompletionAt).isNull() - } - - reports = listOf( - DetailedReport( - UUID.randomUUID(), - null, - null, - null, - null, - Topic.TEST, - "no item count dest", - null, - null, - 0, - null, - true - ), - ).toMutableList() - val testNotDeliveringNoDestination = DetailedSubmissionHistory( - 1, - TaskAction.route, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reports, - ) - testNotDeliveringNoDestination.actionsPerformed = mutableSetOf(TaskAction.route) - testNotDeliveringNoDestination.enrichWithSummary() - testNotDeliveringNoDestination.run { - assertThat(destinationCount).isEqualTo(0) - assertThat(overallStatus).isEqualTo(DetailedSubmissionHistory.Status.NOT_DELIVERING) - assertThat(plannedCompletionAt).isNull() - assertThat(actualCompletionAt).isNull() - } - } - - @Test - fun `test Destination nextActionTime`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.TEST, - "externalName", - null, - OffsetDateTime.now(), - 3, - null, - false - ) - val refUUID = UUID.randomUUID() - val now = OffsetDateTime.now() - val reports = listOf( - inputReport, - DetailedReport( - refUUID, "recvOrg1", - "recvSvc1", - null, - null, - Topic.TEST, - "otherExternalName1", - null, - now, - 1, - 1, - true - ), - DetailedReport( - UUID.randomUUID(), - "recvOrg3", - "recvSvc3", - null, - null, Topic.TEST, - "no item count dest", - null, - now, - 0, - null, - false - ), - ).toMutableList() - - DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - 201, - reports, - emptyList(), - ).run { - // First destination has a transport set therefore sendingAt - assertThat(destinations.first().sendingAt).isEqualTo(now) - assertThat(destinations.last().sendingAt).isNull() - } - } - - @Test - fun `test Status enum toString`() { - assertThat(DetailedSubmissionHistory.Status.RECEIVED.toString()).isEqualTo("Received") - } - - @Test - fun `test UP enrichWithDescendants stopped at route`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - 7, - false - ) - - val refUUID = UUID.randomUUID() - - val reports = listOf( - inputReport, - DetailedReport( - UUID.randomUUID(), - null, - null, - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ), - ).toMutableList() - - val logs = listOf( - DetailedActionLog( - ActionLogScope.translation, - refUUID, - null, - "802798", - ActionLogLevel.filter, - ReportStreamFilterResult( - "recvOrg1.recvSvc1", - 1, - "matches", - listOf( - "ordering_facility_county", - "QUALITY_PASS" - ), - "802798", - ReportStreamFilterType.QUALITY_FILTER - ) - ), - ) - - val testEnrich = DetailedSubmissionHistory( - 2, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reports - ) - assertThat(testEnrich.destinations.count()).isEqualTo(0) - testEnrich.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 1, - TaskAction.route, - OffsetDateTime.now(), - HttpStatus.OK.value(), - null, - logs - ), - ) - ) - - testEnrich.run { - assertThat(destinations.count()).isEqualTo(1) - assertThat(destinations.first().organizationId).isEqualTo("recvOrg1") - assertThat(destinations.first().service).isEqualTo("recvSvc1") - } - } - - @Test - fun `test UP enrichWithDescendants add details from the convert step`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - 7, - false - ) - - val reports = listOf( - inputReport, - DetailedReport( - UUID.randomUUID(), - null, - null, - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ), - ).toMutableList() - - val testEnrich = DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reports - ) - val logs = listOf( - DetailedActionLog( - ActionLogScope.item, - UUID.randomUUID(), - 1, - null, - ActionLogLevel.error, - - FHIRConverter.InvalidItemActionLogDetail( - ErrorCode.INVALID_MSG_VALIDATION, - 0, - "HL7 was not valid at OBX[1]-19[1].1" - ) - ), - ) - assertThat(testEnrich.destinations.count()).isEqualTo(0) - testEnrich.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 2, - TaskAction.convert, - OffsetDateTime.now(), - HttpStatus.OK.value(), - null, - logs - ) - ) - ) - - testEnrich.run { - assertThat(destinations.count()).isEqualTo(0) - assertThat(errors).hasSize(1) - } - } - - @Test - fun `test UP enrichWithDescendants reached translate`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - 7, - false - ) - - val reports = listOf( - inputReport, - DetailedReport( - UUID.randomUUID(), - null, - null, - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ), - ).toMutableList() - - val testEnrich = DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reports - ) - assertThat(testEnrich.destinations.count()).isEqualTo(0) - testEnrich.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 2, - TaskAction.route, - OffsetDateTime.now(), - HttpStatus.OK.value(), - ), - DetailedSubmissionHistory( - 3, - TaskAction.translate, - OffsetDateTime.now(), - HttpStatus.OK.value(), - mutableListOf( - DetailedReport( - UUID.randomUUID(), - "recvOrg1", - "recvSvc1", - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ) - ) - ), - ) - ) - - testEnrich.run { - assertThat(destinations.count()).isEqualTo(1) - assertThat(destinations.first().organizationId).isEqualTo("recvOrg1") - assertThat(destinations.first().service).isEqualTo("recvSvc1") - } - } - - @Test - fun `test UP enrichWithDescendants reached translate multiple report same receiver`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - 7, - false - ) - - val reports = listOf( - inputReport, - DetailedReport( - UUID.randomUUID(), - null, - null, - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ), - ).toMutableList() - - val testEnrich = DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - reports - ) - assertThat(testEnrich.destinations.count()).isEqualTo(0) - testEnrich.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 2, - TaskAction.route, - OffsetDateTime.now(), - HttpStatus.OK.value(), - ), - DetailedSubmissionHistory( - 3, - TaskAction.translate, - OffsetDateTime.now(), - HttpStatus.OK.value(), - mutableListOf( - DetailedReport( - UUID.randomUUID(), - "recvOrg1", - "recvSvc1", - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - null, - true - ), - DetailedReport( - UUID.randomUUID(), - "recvOrg1", - "recvSvc1", - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ), - DetailedReport( - UUID.randomUUID(), - "recvOrg1", - "recvSvc1", - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - null, - true - ) - ) - ), - ) - ) - - testEnrich.run { - assertThat(destinations.count()).isEqualTo(1) - assertThat(destinations.first().organizationId).isEqualTo("recvOrg1") - assertThat(destinations.first().service).isEqualTo("recvSvc1") - assertThat(destinations.first().itemCount).isEqualTo(3) - assertThat(destinations.first().itemCountBeforeQualFilter).isEqualTo(1) - } - } - - @Test - fun `test UP enrichWithDescendants reached translate multiple reports different receivers`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - 7, - false - ) - - val testEnrich = DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - mutableListOf(inputReport) - ) - assertThat(testEnrich.destinations.count()).isEqualTo(0) - testEnrich.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 2, - TaskAction.route, - OffsetDateTime.now(), - HttpStatus.OK.value(), - ), - DetailedSubmissionHistory( - 3, - TaskAction.translate, - OffsetDateTime.now(), - HttpStatus.OK.value(), - mutableListOf( - DetailedReport( - UUID.randomUUID(), - "recvOrg1", - "recvSvc1", - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ), - DetailedReport( - UUID.randomUUID(), - "recvOrg2", - "recvSvc2", - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - 1, - true - ) - ) - ), - ) - ) - - testEnrich.run { - assertThat(destinations.count()).isEqualTo(2) - assertThat(destinations.first().organizationId).isEqualTo("recvOrg1") - assertThat(destinations.first().service).isEqualTo("recvSvc1") - } - } - - @Test - fun `test UP enrichWithDescendants filterLogs populate for the corresponding receiver`() { - val inputReport = DetailedReport( - UUID.randomUUID(), - null, - null, - "org", - "client", - Topic.FULL_ELR, - "externalName", - null, - null, - 5, - null, - false - ) - - val testEnrich = DetailedSubmissionHistory( - 1, - TaskAction.receive, - OffsetDateTime.now(), - HttpStatus.OK.value(), - mutableListOf(inputReport) - ) - - val logs = listOf( - DetailedActionLog( - ActionLogScope.item, - UUID.randomUUID(), - 1, - null, - ActionLogLevel.error, - InvalidEquipmentMessage("") - ), - DetailedActionLog( - ActionLogScope.translation, - UUID.randomUUID(), - 2, - null, - ActionLogLevel.filter, - ReportStreamFilterResult( - "recvOrg1.recvSvc1", - 5, - "matches", - listOf( - "ordering_facility_county", - "QUALITY_PASS" - ), - "802798", - ReportStreamFilterType.QUALITY_FILTER - ) - ), - DetailedActionLog( - ActionLogScope.translation, - UUID.randomUUID(), - 2, - null, - ActionLogLevel.filter, - ReportStreamFilterResult( - "recvOrg2.recvSvc2", - 5, - "matches", - listOf( - "ordering_facility_county", - "QUALITY_PASS" - ), - "802798", - ReportStreamFilterType.QUALITY_FILTER - ) - ), - DetailedActionLog( - ActionLogScope.translation, - UUID.randomUUID(), - 2, - null, - ActionLogLevel.filter, - ReportStreamFilterResult( - "recvOrg1.recvSvc1", - 5, - "matches", - listOf( - "ordering_facility_county", - "QUALITY_PASS" - ), - "802798", - ReportStreamFilterType.QUALITY_FILTER - ) - ), - ) - - assertThat(testEnrich.destinations.count()).isEqualTo(0) - testEnrich.enrichWithDescendants( - listOf( - DetailedSubmissionHistory( - 2, - TaskAction.route, - OffsetDateTime.now(), - HttpStatus.OK.value(), - null, - logs - ), - DetailedSubmissionHistory( - 3, - TaskAction.translate, - OffsetDateTime.now(), - HttpStatus.OK.value(), - mutableListOf( - DetailedReport( - UUID.randomUUID(), - "recvOrg1", - "recvSvc1", - null, - null, - Topic.FULL_ELR, - "otherExternalName1", - null, - null, - 1, - null, - true - ), - ) - ) - ) - ) - - testEnrich.run { - assertThat(destinations.count()).isEqualTo(2) - assertThat(destinations.first().organizationId).isEqualTo("recvOrg1") - assertThat(destinations.first().filteredReportItems?.count()).isEqualTo(2) - assertThat(destinations.first().service).isEqualTo("recvSvc1") - assertThat(destinations[1].service).isEqualTo("recvSvc2") - assertThat(destinations[1].filteredReportItems?.count()).isEqualTo(1) - } - } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/history/azure/SubmissionFunctionIntegrationTests.kt b/prime-router/src/test/kotlin/history/azure/SubmissionFunctionIntegrationTests.kt new file mode 100644 index 00000000000..c215ad78dda --- /dev/null +++ b/prime-router/src/test/kotlin/history/azure/SubmissionFunctionIntegrationTests.kt @@ -0,0 +1,541 @@ +package gov.cdc.prime.router.history.azure + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import gov.cdc.prime.router.ActionLog +import gov.cdc.prime.router.ActionLogLevel +import gov.cdc.prime.router.FileSettings +import gov.cdc.prime.router.InvalidParamMessage +import gov.cdc.prime.router.MimeFormat +import gov.cdc.prime.router.Receiver +import gov.cdc.prime.router.Report +import gov.cdc.prime.router.Sender +import gov.cdc.prime.router.Topic +import gov.cdc.prime.router.azure.DataAccessTransaction +import gov.cdc.prime.router.azure.DatabaseAccess +import gov.cdc.prime.router.azure.MockHttpRequestMessage +import gov.cdc.prime.router.azure.WorkflowEngine +import gov.cdc.prime.router.azure.db.enums.TaskAction +import gov.cdc.prime.router.azure.db.tables.pojos.Action +import gov.cdc.prime.router.azure.db.tables.pojos.ItemLineage +import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile +import gov.cdc.prime.router.azure.db.tables.pojos.ReportLineage +import gov.cdc.prime.router.common.JacksonMapperUtilities +import gov.cdc.prime.router.common.UniversalPipelineTestUtils +import gov.cdc.prime.router.db.ReportStreamTestDatabaseContainer +import gov.cdc.prime.router.db.ReportStreamTestDatabaseSetupExtension +import gov.cdc.prime.router.history.DetailedSubmissionHistory +import gov.cdc.prime.router.tokens.AuthenticatedClaims +import gov.cdc.prime.router.tokens.AuthenticationType +import gov.cdc.prime.router.tokens.oktaSystemAdminGroup +import gov.cdc.prime.router.unittest.UnitTestUtils +import io.mockk.every +import io.mockk.mockkObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.testcontainers.junit.jupiter.Testcontainers +import java.time.OffsetDateTime +import java.util.UUID + +@Testcontainers +@ExtendWith(ReportStreamTestDatabaseSetupExtension::class) +class SubmissionFunctionIntegrationTests { + + class ReportNodeBuilder { + lateinit var theAction: TaskAction + var theReportBlobUrl: String = UUID.randomUUID().toString() + var theItemCount: Int = 1 + val reportGraphNodes: MutableList = mutableListOf() + var receiver: Receiver? = null + var logs: MutableList = mutableListOf() + var theTransportResult: String? = null + + fun receiver(receiver: Receiver) { + this.receiver = receiver + } + + fun action(action: TaskAction) { + this.theAction = action + } + + fun reportBlobUrl(reportBlobUrl: String) { + this.theReportBlobUrl = reportBlobUrl + } + + fun itemCount(itemCount: Int) { + this.theItemCount = itemCount + } + + fun reportGraphNode(initializer: ReportNodeBuilder.() -> Unit) { + this.reportGraphNodes.add(ReportNodeBuilder().apply(initializer)) + } + + fun log(actionLog: ActionLog) { + this.logs.add(actionLog) + } + + fun transportResult(transportResult: String) { + this.theTransportResult = transportResult + } + } + + class ReportGraphNode(val node: ReportFile) { + val children: MutableList = mutableListOf() + } + + class ReportGraphBuilder { + private lateinit var theSubmission: ReportNodeBuilder + private lateinit var theTopic: Topic + private lateinit var theFormat: MimeFormat + private lateinit var theSender: Sender + + fun topic(topic: Topic) { + this.theTopic = topic + } + + fun format(format: MimeFormat) { + this.theFormat = format + } + + fun sender(sender: Sender) { + this.theSender = sender + } + + fun submission(initializer: ReportNodeBuilder.() -> Unit) { + this.theSubmission = ReportNodeBuilder().apply(initializer) + } + + fun generate(dbAccess: DatabaseAccess): ReportGraphNode { + if (!::theTopic.isInitialized) { + throw IllegalStateException("Topic must be set") + } + if (!::theFormat.isInitialized) { + throw IllegalStateException("Format must be set") + } + if (!::theSubmission.isInitialized) { + throw IllegalStateException("Root must be set") + } + val graph = dbAccess.transactReturning { txn -> + val report = Report( + theFormat, + emptyList(), + theSubmission.theItemCount, + metadata = UnitTestUtils.simpleMetadata, + // TODO + nextAction = TaskAction.convert, + topic = theTopic + ) + + val action = Action().setActionName(theSubmission.theAction).setExternalName("") + action.setSendingOrg(theSender.organizationName) + action.setSendingOrgClient(theSender.name) + action.setHttpStatus(201) + val actionId = dbAccess.insertAction(txn, action) + action.actionId = actionId + val reportFile = ReportFile() + .setSchemaTopic(theTopic) + .setReportId(report.id) + .setActionId(actionId) + .setSchemaName("") + .setSendingOrg(theSender.organizationName) + .setSendingOrgClient(theSender.name) + .setBodyFormat(theFormat.toString()) + .setItemCount(theSubmission.theItemCount) + .setExternalName("test-external-name") + .setBodyUrl(theSubmission.theReportBlobUrl) + .setNextAction(theSubmission.reportGraphNodes.firstOrNull()?.theAction) + dbAccess.insertReportFile( + reportFile, txn, action + ) + + val graph = ReportGraphNode(reportFile) + + theSubmission.reportGraphNodes.foldIndexed(graph) { nodeIndex, acc, node -> + acc.children.add(descend(node, dbAccess, txn, report, graph, nodeIndex)) + acc + } + graph + } + return graph + } + + private fun descend( + node: ReportNodeBuilder, + dbAccess: DatabaseAccess, + txn: DataAccessTransaction, + report: Report, + graph: ReportGraphNode, + nodeIndex: Int, + ): ReportGraphNode { + val childReport = Report( + theFormat, + emptyList(), + node.theItemCount, + metadata = UnitTestUtils.simpleMetadata, + nextAction = TaskAction.convert, + topic = theTopic + ) + val childAction = Action().setActionName(node.theAction).setExternalName("") + + if (node.receiver != null) { + childAction.setReceivingOrg(node.receiver!!.organizationName) + childAction.setReceivingOrgSvc(node.receiver!!.name) + } + val childActionId = dbAccess.insertAction(txn, childAction) + childAction.actionId = childActionId + val childReportFile = ReportFile() + .setSchemaTopic(theTopic) + .setReportId(childReport.id) + .setActionId(childActionId) + .setSchemaName("") + .setBodyFormat(theFormat.toString()) + .setItemCount(node.theItemCount) + .setExternalName("test-external-name") + .setBodyUrl(node.theReportBlobUrl) + .setTransportResult(node.theTransportResult) + .setNextAction(node.reportGraphNodes.firstOrNull()?.theAction) + + if (node.receiver != null) { + childReportFile.setReceivingOrg(node.receiver!!.organizationName) + childReportFile.setReceivingOrgSvc(node.receiver!!.name) + } + dbAccess.insertReportFile( + childReportFile, txn, childAction + ) + node.logs.forEach { log -> + log.action = childAction + log.reportId = childReport.id + dbAccess.insertActionLog(log, txn) + } + + dbAccess.insertReportLineage( + ReportLineage( + null, + childActionId, + graph.node.reportId, + childReportFile.reportId, + OffsetDateTime.now() + ), + txn + ) + + dbAccess.insertItemLineages( + setOf( + ItemLineage( + null, + graph.node.reportId, + 1, + childReportFile.reportId, + nodeIndex + 1, + null, + null, + null, + "" + ) + ), + txn, childAction + ) + val childGraph = ReportGraphNode(childReportFile) + node.reportGraphNodes.foldIndexed(graph) { childNodeIndex, acc, descendant -> + descend(descendant, dbAccess, txn, report, childGraph, childNodeIndex) + acc + } + return childGraph + } + } + + fun reportGraph(initializer: ReportGraphBuilder.() -> Unit): ReportGraphBuilder { + return ReportGraphBuilder().apply(initializer) + } + + @Test + fun `it should return a history for partially delivered submission`() { + val submittedReport = reportGraph { + topic(Topic.FULL_ELR) + format(MimeFormat.HL7) + sender(UniversalPipelineTestUtils.hl7Sender) + + submission { + action(TaskAction.receive) + reportGraphNode { + action(TaskAction.convert) + log(ActionLog(InvalidParamMessage("log"), type = ActionLogLevel.warning)) + reportGraphNode { + action(TaskAction.route) + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + itemCount(0) + } + } + reportGraphNode { + action(TaskAction.route) + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + reportGraphNode { + action(TaskAction.send) + transportResult("Success") + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + } + } + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + itemCount(0) + } + } + } + } + }.generate(ReportStreamTestDatabaseContainer.testDatabaseAccess) + + val httpRequestMessage = MockHttpRequestMessage() + + val func = setupSubmissionFunction() + + val history = func + .getReportDetailedHistory(httpRequestMessage, submittedReport.node.reportId.toString()) + assertThat(history).isNotNull() + val historyNode = JacksonMapperUtilities.defaultMapper.readTree(history.body.toString()) + assertThat( + historyNode.get("overallStatus").asText() + ).isEqualTo(DetailedSubmissionHistory.Status.PARTIALLY_DELIVERED.toString()) + assertThat(historyNode.get("destinations").size()).isEqualTo(2) + assertThat(historyNode.get("errors").size()).isEqualTo(0) + assertThat(historyNode.get("warnings").size()).isEqualTo(1) + } + + @Test + fun `it should return a history that a submission has been received`() { + val submittedReport = reportGraph { + topic(Topic.FULL_ELR) + format(MimeFormat.HL7) + sender(UniversalPipelineTestUtils.hl7Sender) + + submission { + action(TaskAction.receive) + } + }.generate(ReportStreamTestDatabaseContainer.testDatabaseAccess) + + val httpRequestMessage = MockHttpRequestMessage() + + val func = setupSubmissionFunction() + + val history = func + .getReportDetailedHistory(httpRequestMessage, submittedReport.node.reportId.toString()) + assertThat(history).isNotNull() + val historyNode = JacksonMapperUtilities.defaultMapper.readTree(history.body.toString()) + assertThat( + historyNode.get("overallStatus").asText() + ).isEqualTo(DetailedSubmissionHistory.Status.RECEIVED.toString()) + assertThat(historyNode.get("destinations").size()).isEqualTo(0) + assertThat(historyNode.get("errors").size()).isEqualTo(0) + assertThat(historyNode.get("warnings").size()).isEqualTo(0) + } + + @Test + fun `it should return a history that indicates the report is not going to be delivered`() { + val submittedReport = reportGraph { + topic(Topic.FULL_ELR) + format(MimeFormat.HL7) + sender(UniversalPipelineTestUtils.hl7Sender) + + submission { + action(TaskAction.receive) + reportGraphNode { + action(TaskAction.convert) + log(ActionLog(InvalidParamMessage("log"), type = ActionLogLevel.warning)) + reportGraphNode { + action(TaskAction.route) + } + } + } + }.generate(ReportStreamTestDatabaseContainer.testDatabaseAccess) + + val httpRequestMessage = MockHttpRequestMessage() + + val func = setupSubmissionFunction() + + val history = func + .getReportDetailedHistory(httpRequestMessage, submittedReport.node.reportId.toString()) + assertThat(history).isNotNull() + val historyNode = JacksonMapperUtilities.defaultMapper.readTree(history.body.toString()) + assertThat( + historyNode.get("overallStatus").asText() + ).isEqualTo(DetailedSubmissionHistory.Status.NOT_DELIVERING.toString()) + assertThat(historyNode.get("destinations").size()).isEqualTo(0) + assertThat(historyNode.get("errors").size()).isEqualTo(0) + assertThat(historyNode.get("warnings").size()).isEqualTo(1) + } + + @Test + fun `it should return a history that indicates waiting to deliver`() { + val submittedReport = reportGraph { + topic(Topic.FULL_ELR) + format(MimeFormat.HL7) + sender(UniversalPipelineTestUtils.hl7Sender) + + submission { + action(TaskAction.receive) + reportGraphNode { + action(TaskAction.convert) + log(ActionLog(InvalidParamMessage("log"), type = ActionLogLevel.warning)) + reportGraphNode { + action(TaskAction.route) + log(ActionLog(InvalidParamMessage("log"), type = ActionLogLevel.error)) + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + reportGraphNode { + action(TaskAction.send) + transportResult("Success") + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + } + } + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + } + } + reportGraphNode { + action(TaskAction.route) + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + reportGraphNode { + action(TaskAction.send) + transportResult("Success") + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + } + } + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + } + } + } + } + }.generate(ReportStreamTestDatabaseContainer.testDatabaseAccess) + + val httpRequestMessage = MockHttpRequestMessage() + + val func = setupSubmissionFunction() + + val history = func + .getReportDetailedHistory(httpRequestMessage, submittedReport.node.reportId.toString()) + assertThat(history).isNotNull() + val historyNode = JacksonMapperUtilities.defaultMapper.readTree(history.body.toString()) + assertThat( + historyNode.get("overallStatus").asText() + ).isEqualTo(DetailedSubmissionHistory.Status.WAITING_TO_DELIVER.toString()) + assertThat(historyNode.get("destinations").size()).isEqualTo(2) + assertThat(historyNode.get("errors").size()).isEqualTo(1) + assertThat(historyNode.get("warnings").size()).isEqualTo(1) + } + + @Test + fun `it should return history of a submission that is delivered`() { + val submittedReport = reportGraph { + topic(Topic.FULL_ELR) + format(MimeFormat.HL7) + sender(UniversalPipelineTestUtils.hl7Sender) + + submission { + action(TaskAction.receive) + reportGraphNode { + action(TaskAction.convert) + log(ActionLog(InvalidParamMessage("log"), type = ActionLogLevel.warning)) + reportGraphNode { + action(TaskAction.route) + log(ActionLog(InvalidParamMessage("log"), type = ActionLogLevel.error)) + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + reportGraphNode { + action(TaskAction.send) + transportResult("Success") + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + } + } + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + reportGraphNode { + action(TaskAction.send) + transportResult("Success") + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + } + } + } + reportGraphNode { + action(TaskAction.route) + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + reportGraphNode { + action(TaskAction.send) + transportResult("Success") + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[0]) + } + } + reportGraphNode { + action(TaskAction.translate) + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + reportGraphNode { + action(TaskAction.send) + transportResult("Success") + receiver(UniversalPipelineTestUtils.universalPipelineOrganization.receivers[1]) + } + } + } + } + } + }.generate(ReportStreamTestDatabaseContainer.testDatabaseAccess) + + val httpRequestMessage = MockHttpRequestMessage() + + val func = setupSubmissionFunction() + + val history = func + .getReportDetailedHistory(httpRequestMessage, submittedReport.node.reportId.toString()) + assertThat(history).isNotNull() + val historyNode = JacksonMapperUtilities.defaultMapper.readTree(history.body.toString()) + assertThat( + historyNode.get("overallStatus").asText() + ).isEqualTo(DetailedSubmissionHistory.Status.DELIVERED.toString()) + assertThat(historyNode.get("destinations").size()).isEqualTo(2) + assertThat(historyNode.get("errors").size()).isEqualTo(1) + assertThat(historyNode.get("warnings").size()).isEqualTo(1) + assertThat(historyNode.get("sender").asText()).isEqualTo("phd.elr-hl7-sender") + assertThat(historyNode.get("actualCompletionAt").asText()).isNotNull() + } + + @BeforeEach + fun setupAuth() { + val jwt = mapOf("organization" to listOf(oktaSystemAdminGroup), "sub" to "test@cdc.gov") + val claims = AuthenticatedClaims(jwt, AuthenticationType.Okta) + mockkObject(AuthenticatedClaims) + every { AuthenticatedClaims.authenticate(any()) } returns claims + } + + private fun setupSubmissionFunction(): SubmissionFunction { + val workflowEngine = WorkflowEngine + .Builder() + .metadata(UnitTestUtils.simpleMetadata) + .settingsProvider( + FileSettings().loadOrganizations(UniversalPipelineTestUtils.universalPipelineOrganization) + ) + .databaseAccess(ReportStreamTestDatabaseContainer.testDatabaseAccess) + .build() + return SubmissionFunction( + SubmissionsFacade( + DatabaseSubmissionsAccess(ReportStreamTestDatabaseContainer.testDatabaseAccess), + ReportStreamTestDatabaseContainer.testDatabaseAccess + ), + workflowEngine, + + ) + } +} \ No newline at end of file diff --git a/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt b/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt index 239d3226be8..43317c09259 100644 --- a/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt +++ b/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt @@ -457,7 +457,7 @@ class SubmissionFunctionTests : Logging { // Good return val returnBody = DetailedSubmissionHistory( 550, TaskAction.receive, OffsetDateTime.now(), 201, - null + mutableListOf(), emptyList() ) // Happy path with a good UUID val action = Action() @@ -466,8 +466,9 @@ class SubmissionFunctionTests : Logging { action.actionName = TaskAction.receive every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID - every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns returnBody + every { mockSubmissionFacade.findDetailedSubmissionHistory(any(), any(), any()) } returns returnBody every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true + every { mockSubmissionFacade.fetchReportForActionId(any(), any()) } returns null response = function.getReportDetailedHistory(mockRequest, goodUuid) assertThat(response.status).isEqualTo(HttpStatus.OK) var responseBody: DetailSubmissionHistoryResponse = mapper.readValue(response.body.toString()) @@ -498,7 +499,7 @@ class SubmissionFunctionTests : Logging { // Happy path with a good actionId every { mockSubmissionFacade.fetchActionForReportId(any()) } returns null // not used for an actionId every { mockSubmissionFacade.fetchAction(any()) } returns action - every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns returnBody + every { mockSubmissionFacade.findDetailedSubmissionHistory(any(), any(), any()) } returns returnBody every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true response = function.getReportDetailedHistory(mockRequest, goodActionId) assertThat(response.status).isEqualTo(HttpStatus.OK) @@ -678,7 +679,7 @@ class SubmissionFunctionTests : Logging { every { anyConstructed().getDescendantReports(any(), any(), any()) } returns emptyList() every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID - every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns null + every { mockSubmissionFacade.findDetailedSubmissionHistory(any(), any(), any()) } returns null every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true val restCreds = mockk() @@ -724,13 +725,15 @@ class SubmissionFunctionTests : Logging { val detailedReport = DetailedReport( UUID.randomUUID(), "flexion", "flexion", "lab", "lab", Topic.ETOR_TI, "external", - null, null, 1, 1, true + null, null, 1, 1, true, null, + null, + null ) // Good return val returnBody = DetailedSubmissionHistory( 550, TaskAction.receive, OffsetDateTime.now(), 201, - mutableListOf(detailedReport) + mutableListOf(detailedReport), emptyList() ) returnBody.destinations = listOf( @@ -762,7 +765,7 @@ class SubmissionFunctionTests : Logging { } returns listOf(firstReport, secondReport) every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID - every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns returnBody + every { mockSubmissionFacade.findDetailedSubmissionHistory(any(), any(), any()) } returns returnBody every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true val restCreds = mockk() @@ -815,13 +818,15 @@ class SubmissionFunctionTests : Logging { val detailedReport = DetailedReport( UUID.randomUUID(), "flexion", "flexion", "lab", "lab", Topic.ETOR_TI, "external", - null, null, 1, 1, true + null, null, 1, 1, true, null, + null, + null ) // Good return val returnBody = DetailedSubmissionHistory( 550, TaskAction.receive, OffsetDateTime.now(), 201, - mutableListOf(detailedReport) + mutableListOf(detailedReport), emptyList() ) returnBody.destinations = listOf( @@ -853,7 +858,7 @@ class SubmissionFunctionTests : Logging { } returns listOf(firstReport, secondReport) every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID - every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns returnBody + every { mockSubmissionFacade.findDetailedSubmissionHistory(any(), any(), any()) } returns returnBody every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true val restCreds = mockk() diff --git a/prime-router/src/test/kotlin/history/azure/SubmissionsFacadeTests.kt b/prime-router/src/test/kotlin/history/azure/SubmissionsFacadeTests.kt index d9d13a55e9a..d2501faf4a0 100644 --- a/prime-router/src/test/kotlin/history/azure/SubmissionsFacadeTests.kt +++ b/prime-router/src/test/kotlin/history/azure/SubmissionsFacadeTests.kt @@ -3,21 +3,15 @@ package gov.cdc.prime.router.history.azure import assertk.assertFailure import assertk.assertThat import assertk.assertions.hasMessage -import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import com.google.common.net.HttpHeaders import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.MockHttpRequestMessage -import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.Action -import gov.cdc.prime.router.history.DetailedSubmissionHistory import gov.cdc.prime.router.tokens.AuthenticatedClaims import gov.cdc.prime.router.tokens.AuthenticationType -import io.mockk.every import io.mockk.mockk -import java.time.OffsetDateTime -import java.util.UUID import kotlin.test.Test class SubmissionsFacadeTests { @@ -56,50 +50,6 @@ class SubmissionsFacadeTests { }.hasMessage("Invalid organization.") } - @Test - fun `test findDetailedSubmissionHistory`() { - val mockSubmissionAccess = mockk() - val mockDbAccess = mockk() - val facade = SubmissionsFacade(mockSubmissionAccess, mockDbAccess) - // Good return - val goodReturn = DetailedSubmissionHistory( - 550, TaskAction.receive, OffsetDateTime.now(), - null, null, emptyList() - ) - every { - mockSubmissionAccess.fetchAction( - any(), - any(), - DetailedSubmissionHistory::class.java - ) - } returns goodReturn - - // No lineage since we have no report ID - val action1 = Action() - action1.actionId = 550 - action1.sendingOrg = "myOrg" - action1.actionName = TaskAction.receive - assertThat(facade.findDetailedSubmissionHistory(action1)).isEqualTo(goodReturn) - - // Happy path - goodReturn.reportId = UUID.randomUUID().toString() - every { - mockSubmissionAccess.fetchRelatedActions( - UUID.fromString(goodReturn.reportId), DetailedSubmissionHistory::class.java - ) - } returns emptyList() - assertThat(facade.findDetailedSubmissionHistory(action1)).isEqualTo(goodReturn) - // Failures - val action2 = Action() - action2.actionId = 550 - action2.sendingOrg = "myOrg" // good - action2.actionName = TaskAction.process // bad. Submission queries only work on receive actions. - assertFailure { facade.findDetailedSubmissionHistory(action2) } // not a receive action - action2.actionName = TaskAction.receive // good - action2.sendingOrg = null // bad - assertFailure { facade.findDetailedSubmissionHistory(action2) } // missing sendingOrg - } - @Test fun `test checkAccessAuthorizationForOrg`() { val mockSubmissionAccess = mockk()