diff --git a/prime-router/src/main/kotlin/azure/SenderFunction.kt b/prime-router/src/main/kotlin/azure/SenderFunction.kt new file mode 100644 index 00000000000..2b19e42369d --- /dev/null +++ b/prime-router/src/main/kotlin/azure/SenderFunction.kt @@ -0,0 +1,84 @@ +package gov.cdc.prime.router.azure + +import com.fasterxml.jackson.databind.ObjectMapper +import com.github.doyaaaaaken.kotlincsv.dsl.csvReader +import com.microsoft.azure.functions.HttpMethod +import com.microsoft.azure.functions.HttpRequestMessage +import com.microsoft.azure.functions.HttpResponseMessage +import com.microsoft.azure.functions.annotation.AuthorizationLevel +import com.microsoft.azure.functions.annotation.FunctionName +import com.microsoft.azure.functions.annotation.HttpTrigger +import gov.cdc.prime.router.azure.db.enums.TaskAction +import gov.cdc.prime.router.cli.LookupTableCompareMappingCommand +import gov.cdc.prime.router.metadata.ObservationMappingConstants +import gov.cdc.prime.router.tokens.AuthenticatedClaims +import gov.cdc.prime.router.tokens.authenticationFailure +import gov.cdc.prime.router.tokens.authorizationFailure +import org.apache.logging.log4j.kotlin.Logging + +class SenderFunction( + private val workflowEngine: WorkflowEngine = WorkflowEngine(), + private val actionHistory: ActionHistory = ActionHistory(TaskAction.receive), +) : RequestFunction(workflowEngine), + Logging { + + /** + * POST a CSV with test codes and conditions to compare with existing + * code to condition observation mapping table + * + * @return original request body data with mapping results in JSON format + */ + @FunctionName("conditionCodeComparisonPostRequest") + fun conditionCodeComparisonPostRequest( + @HttpTrigger( + name = "conditionCodeComparisonPostRequest", + methods = [HttpMethod.POST], + authLevel = AuthorizationLevel.ANONYMOUS, + route = "sender/conditionCode/comparison" + ) request: HttpRequestMessage, + ): HttpResponseMessage { + val senderName = extractClient(request) + if (senderName.isBlank()) { + return HttpUtilities.bad(request, "Expected a '$CLIENT_PARAMETER' query parameter") + } + + actionHistory.trackActionParams(request) + try { + val claims = AuthenticatedClaims.authenticate(request) + ?: return HttpUtilities.unauthorizedResponse(request, authenticationFailure) + + val sender = workflowEngine.settings.findSender(senderName) + ?: return HttpUtilities.bad(request, "'$CLIENT_PARAMETER:$senderName': unknown client") + + if (!claims.authorizedForSendOrReceive(sender, request)) { + return HttpUtilities.unauthorizedResponse(request, authorizationFailure) + } + + // Read request body CSV + val bodyCsvText = request.body ?: "" + val bodyCsv = csvReader().readAllWithHeader(bodyCsvText) + + // Get observation mapping table + val tableMapper = LookupTableConditionMapper(workflowEngine.metadata) + val observationMappingTable = tableMapper.mappingTable.caseSensitiveDataRowsMap + val tableTestCodeMap = observationMappingTable.associateBy { it[ObservationMappingConstants.TEST_CODE_KEY] } + + // Compare request CSV with table using CLI wrapper + val conditionCodeComparison = LookupTableCompareMappingCommand.compareMappings( + compendium = bodyCsv, tableTestCodeMap = tableTestCodeMap + ) + + // Create output JSON with mapping comparison result + val conditionCodeComparisonJson = ObjectMapper().writeValueAsString(conditionCodeComparison) + + return HttpUtilities.okResponse(request, conditionCodeComparisonJson) + } catch (ex: Exception) { + if (ex.message != null) { + logger.error(ex.message!!, ex) + } else { + logger.error(ex) + } + return HttpUtilities.internalErrorResponse(request) + } + } +} \ No newline at end of file diff --git a/prime-router/src/test/kotlin/azure/SenderFunctionTest.kt b/prime-router/src/test/kotlin/azure/SenderFunctionTest.kt new file mode 100644 index 00000000000..2e5845dbe72 --- /dev/null +++ b/prime-router/src/test/kotlin/azure/SenderFunctionTest.kt @@ -0,0 +1,292 @@ +package gov.cdc.prime.router.azure + +import com.microsoft.azure.functions.HttpStatus +import gov.cdc.prime.router.CustomerStatus +import gov.cdc.prime.router.DeepOrganization +import gov.cdc.prime.router.FileSettings +import gov.cdc.prime.router.Metadata +import gov.cdc.prime.router.MimeFormat +import gov.cdc.prime.router.Organization +import gov.cdc.prime.router.Receiver +import gov.cdc.prime.router.SettingsProvider +import gov.cdc.prime.router.Topic +import gov.cdc.prime.router.UniversalPipelineSender +import gov.cdc.prime.router.azure.db.enums.TaskAction +import gov.cdc.prime.router.cli.LookupTableCompareMappingCommand +import gov.cdc.prime.router.metadata.LookupTable +import gov.cdc.prime.router.metadata.ObservationMappingConstants +import gov.cdc.prime.router.serializers.Hl7Serializer +import gov.cdc.prime.router.tokens.AuthenticatedClaims +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 +import org.jooq.tools.jdbc.MockConnection +import org.jooq.tools.jdbc.MockDataProvider +import org.jooq.tools.jdbc.MockResult +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class SenderFunctionTest { + val dataProvider = MockDataProvider { emptyArray() } + val connection = MockConnection(dataProvider) + val accessSpy = spyk(DatabaseAccess(connection)) + val metadata = UnitTestUtils.simpleMetadata + val settings = mockkClass(SettingsProvider::class) + val blobMock = mockkClass(BlobAccess::class) + private val serializer = spyk(Hl7Serializer(metadata, settings)) + private val queueMock = mockkClass(QueueAccess::class) + private val timing1 = mockkClass(Receiver.Timing::class) + + val REQ_BODY_TEST_CSV = "test code,test description,coding system\n" + + "97097-0,SARS-CoV-2 (COVID-19) Ag [Presence] in Upper respiratory specimen by Rapid immunoassay,LOINC\n" + + "80382-5,Influenza virus A Ag [Presence] in Upper respiratory specimen by Rapid immunoassay,LOINC\n" + + "12345,Flu B,LOCAL" + + val testOrganization = DeepOrganization( + "phd", + "test", + Organization.Jurisdiction.FEDERAL, + receivers = listOf( + Receiver( + "elr", + "phd", + Topic.TEST, + CustomerStatus.INACTIVE, + "one", + timing = timing1 + ) + ) + ) + + private fun makeEngine(metadata: Metadata, settings: SettingsProvider): WorkflowEngine = spyk( + WorkflowEngine.Builder().metadata(metadata).settingsProvider(settings).databaseAccess(accessSpy) + .blobAccess(blobMock).queueAccess(queueMock).hl7Serializer(serializer).build() + ) + + @BeforeEach + fun reset() { + clearAllMocks() + + // setup + every { timing1.isValid() } returns true + every { timing1.numberPerDay } returns 1 + every { timing1.maxReportCount } returns 1 + every { timing1.whenEmpty } returns Receiver.WhenEmpty() + } + + @Test + fun `test SenderFunction conditionCodeComparisonPostRequest ok`() { + val metadata = UnitTestUtils.simpleMetadata + val settings = FileSettings().loadOrganizations(testOrganization) + val sender = UniversalPipelineSender( + name = "Test Sender", + organizationName = "testOrganization", + format = MimeFormat.HL7, + topic = Topic.FULL_ELR + ) + + val workflowEngine = makeEngine(metadata, settings) + val actionHistory = spyk(ActionHistory(TaskAction.receive)) + val senderFunction = spyk(SenderFunction(workflowEngine, actionHistory)) + + val testRequest = MockHttpRequestMessage(REQ_BODY_TEST_CSV) + + every { workflowEngine.settings.findSender("Test Sender") } returns sender + + mockkObject(AuthenticatedClaims) + val mockClaims = mockk() + every { AuthenticatedClaims.authenticate(any()) } returns mockClaims + every { mockClaims.authorizedForSendOrReceive(any(), any()) } returns true + + metadata.lookupTableStore += mapOf( + "observation-mapping" to LookupTable( + "observation-mapping", + listOf( + listOf( + ObservationMappingConstants.TEST_CODE_KEY, + ObservationMappingConstants.CONDITION_CODE_KEY, + ObservationMappingConstants.CONDITION_CODE_SYSTEM_KEY, + ObservationMappingConstants.CONDITION_NAME_KEY + ), + listOf( + "00001", + "Some Condition Code", + "Condition Code System", + "Condition Name" + ) + ) + ) + ) + + val codeToConditionMapping = listOf( + mapOf( + "test code" to "00001", + "test description" to "test description 1", + "coding system" to "Condition Code System", + "mapped?" to "Y" + ), + mapOf( + "test code" to "00002", + "test description" to "test description 2", + "coding system" to "Another Condition Code System", + "mapped?" to "N" + ) + ) + mockkObject(LookupTableCompareMappingCommand) + every { + LookupTableCompareMappingCommand.compareMappings(any(), any()) + } returns codeToConditionMapping + + testRequest.httpHeaders += mapOf( + "client" to "Test Sender", + "content-length" to "4" + ) + + val response = senderFunction.conditionCodeComparisonPostRequest(testRequest) + + assertEquals(HttpStatus.OK, response.status) + } + + @Test + fun `test SenderFunction conditionCodeComparisonPostRequest with no sender`() { + val metadata = UnitTestUtils.simpleMetadata + val settings = FileSettings().loadOrganizations(testOrganization) + + val workflowEngine = makeEngine(metadata, settings) + val actionHistory = spyk(ActionHistory(TaskAction.receive)) + val senderFunction = spyk(SenderFunction(workflowEngine, actionHistory)) + + val testRequest = MockHttpRequestMessage(REQ_BODY_TEST_CSV) + + testRequest.httpHeaders += mapOf( + "content-length" to "4" + ) + + val response = senderFunction.conditionCodeComparisonPostRequest(testRequest) + + assertEquals(HttpStatus.BAD_REQUEST, response.status) + } + + @Test + fun `test SenderFunction conditionCodeComparisonPostRequest with bad sender`() { + val metadata = UnitTestUtils.simpleMetadata + val settings = FileSettings().loadOrganizations(testOrganization) + + val workflowEngine = makeEngine(metadata, settings) + val actionHistory = spyk(ActionHistory(TaskAction.receive)) + val senderFunction = spyk(SenderFunction(workflowEngine, actionHistory)) + + val testRequest = MockHttpRequestMessage(REQ_BODY_TEST_CSV) + + every { workflowEngine.settings.findSender("Test sender") } returns null + + mockkObject(AuthenticatedClaims) + val mockClaims = mockk() + every { AuthenticatedClaims.authenticate(any()) } returns mockClaims + + testRequest.httpHeaders += mapOf( + "client" to "Test sender", + "content-length" to "4" + ) + + val response = senderFunction.conditionCodeComparisonPostRequest(testRequest) + + assertEquals(HttpStatus.BAD_REQUEST, response.status) + } + + @Test + fun `test SenderFunction conditionCodeComparisonPostRequest with unauthenticated sender`() { + val metadata = UnitTestUtils.simpleMetadata + val settings = FileSettings().loadOrganizations(testOrganization) + + val workflowEngine = makeEngine(metadata, settings) + val actionHistory = spyk(ActionHistory(TaskAction.receive)) + val senderFunction = spyk(SenderFunction(workflowEngine, actionHistory)) + + val testRequest = MockHttpRequestMessage(REQ_BODY_TEST_CSV) + + every { workflowEngine.settings.findSender("Test sender") } returns null + + mockkObject(AuthenticatedClaims) + every { AuthenticatedClaims.authenticate(any()) } returns null + + testRequest.httpHeaders += mapOf( + "client" to "Test sender", + "content-length" to "4" + ) + + val response = senderFunction.conditionCodeComparisonPostRequest(testRequest) + + assertEquals(HttpStatus.UNAUTHORIZED, response.status) + } + + @Test + fun `test SenderFunction conditionCodeComparisonPostRequest with unauthorized sender`() { + val metadata = UnitTestUtils.simpleMetadata + val settings = FileSettings().loadOrganizations(testOrganization) + val sender = UniversalPipelineSender( + name = "Test Sender", + organizationName = "testOrganization", + format = MimeFormat.HL7, + topic = Topic.FULL_ELR + ) + + val workflowEngine = makeEngine(metadata, settings) + val actionHistory = spyk(ActionHistory(TaskAction.receive)) + val senderFunction = spyk(SenderFunction(workflowEngine, actionHistory)) + + val testRequest = MockHttpRequestMessage(REQ_BODY_TEST_CSV) + + every { workflowEngine.settings.findSender("Test sender") } returns sender + + mockkObject(AuthenticatedClaims) + val mockClaims = mockk() + every { AuthenticatedClaims.authenticate(any()) } returns mockClaims + every { mockClaims.authorizedForSendOrReceive(any(), any()) } returns false + + testRequest.httpHeaders += mapOf( + "client" to "Test sender", + "content-length" to "4" + ) + + val response = senderFunction.conditionCodeComparisonPostRequest(testRequest) + + assertEquals(HttpStatus.UNAUTHORIZED, response.status) + } + + @Test + fun `test SenderFunction conditionCodeComparisonPostRequest exception error`() { + val metadata = UnitTestUtils.simpleMetadata + val settings = FileSettings().loadOrganizations(testOrganization) + val sender = UniversalPipelineSender( + name = "Test Sender", + organizationName = "testOrganization", + format = MimeFormat.HL7, + topic = Topic.FULL_ELR + ) + + val workflowEngine = makeEngine(metadata, settings) + val actionHistory = spyk(ActionHistory(TaskAction.receive)) + val senderFunction = spyk(SenderFunction(workflowEngine, actionHistory)) + + val testRequest = MockHttpRequestMessage(REQ_BODY_TEST_CSV) + + every { workflowEngine.settings.findSender("Test sender") } returns sender + mockkObject(LookupTableCompareMappingCommand) + every { LookupTableCompareMappingCommand.compareMappings(any(), any()) }.throws(Exception()) + + testRequest.httpHeaders += mapOf( + "client" to "Test sender", + "content-length" to "4" + ) + + val response = senderFunction.conditionCodeComparisonPostRequest(testRequest) + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.status) + } +} \ No newline at end of file