Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Engagement/jessica/15545 test bank return messages api #15736

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion prime-router/docs/api/reports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,22 @@ paths:
$ref: '#/components/schemas/Report'
'500':
description: Internal Server Error

/reports/testing:
get:
summary: Gets the message from the test message bank in the environment
security:
- OAuth2: [ system_admin ]
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Report'
'500':
description: Internal Server Error
# Building
components:
schemas:
Expand Down
70 changes: 70 additions & 0 deletions prime-router/src/main/kotlin/azure/ReportFunction.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package gov.cdc.prime.router.azure

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.google.common.net.HttpHeaders
import com.microsoft.azure.functions.HttpMethod
import com.microsoft.azure.functions.HttpRequestMessage
Expand All @@ -19,6 +22,8 @@ import gov.cdc.prime.router.Sender
import gov.cdc.prime.router.Sender.ProcessingType
import gov.cdc.prime.router.SubmissionReceiver
import gov.cdc.prime.router.UniversalPipelineReceiver
import gov.cdc.prime.router.azure.BlobAccess.Companion.defaultBlobMetadata
import gov.cdc.prime.router.azure.BlobAccess.Companion.getBlobContainer
import gov.cdc.prime.router.azure.db.enums.TaskAction
import gov.cdc.prime.router.azure.observability.event.IReportStreamEventService
import gov.cdc.prime.router.azure.observability.event.ReportStreamEventName
Expand All @@ -28,6 +33,7 @@ import gov.cdc.prime.router.common.AzureHttpUtils.getSenderIP
import gov.cdc.prime.router.common.JacksonMapperUtilities
import gov.cdc.prime.router.history.azure.SubmissionsFacade
import gov.cdc.prime.router.tokens.AuthenticatedClaims
import gov.cdc.prime.router.tokens.Scope
import gov.cdc.prime.router.tokens.authenticationFailure
import gov.cdc.prime.router.tokens.authorizationFailure
import org.apache.logging.log4j.kotlin.Logging
Expand Down Expand Up @@ -85,6 +91,70 @@ class ReportFunction(
}
}

/**
* GET messages from test bank
*
* @see ../../../docs/api/reports.yml
*/
@FunctionName("getMessagesFromTestBank")
fun getMessagesFromTestBank(
@HttpTrigger(
name = "getMessagesFromTestBank",
methods = [HttpMethod.GET],
authLevel = AuthorizationLevel.ANONYMOUS,
route = "reports/testing"
) request: HttpRequestMessage<String?>,
): HttpResponseMessage {
val claims = AuthenticatedClaims.authenticate(request)
if (claims != null && claims.authorized(setOf(Scope.primeAdminScope))) {
return processGetMessageFromTestBankRequest(request)
}
return HttpUtilities.unauthorizedResponse(request)
}

/**
* Moved the logic to a separate function for testing purposes
*/
fun processGetMessageFromTestBankRequest(
request: HttpRequestMessage<String?>,
blobAccess: BlobAccess.Companion = BlobAccess,
defaultBlobMetadata: BlobAccess.BlobContainerMetadata = BlobAccess.defaultBlobMetadata,
): HttpResponseMessage {
return try {
val updatedBlobMetadata = defaultBlobMetadata.copy(containerName = "test-bank")
val results = blobAccess.listBlobs("", updatedBlobMetadata)
val reports = mutableListOf<TestReportInfo>()
val sourceContainer = getBlobContainer(updatedBlobMetadata)
results.forEach { currentResult ->
if (currentResult.currentBlobItem.name.endsWith(".fhir")) {
val sourceBlobClient = sourceContainer.getBlobClient(currentResult.currentBlobItem.name)
val data = sourceBlobClient.downloadContent()

val currentTestReportInfo = TestReportInfo(
currentResult.currentBlobItem.properties.creationTime.toString(),
currentResult.currentBlobItem.name,
data.toString()
)
reports.add(currentTestReportInfo)
}
}

val mapper: ObjectMapper = JsonMapper.builder()
.addModule(JavaTimeModule())
.build()
HttpUtilities.okResponse(request, mapper.writeValueAsString(reports) ?: "[]")
} catch (e: Exception) {
logger.error("Unable to fetch messages from test bank", e)
HttpUtilities.internalErrorResponse(request)
}
}

class TestReportInfo(
var dateCreated: String,
var fileName: String,
var reportBody: String,
)

/**
* The Waters API, in memory of Dr. Michael Waters
* (The older version of this API is "/api/reports")
Expand Down
94 changes: 94 additions & 0 deletions prime-router/src/test/kotlin/azure/ReportFunctionTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ package gov.cdc.prime.router.azure

import assertk.assertThat
import assertk.assertions.isEqualTo
import com.azure.core.util.BinaryData
import com.azure.storage.blob.BlobClient
import com.azure.storage.blob.BlobContainerClient
import com.azure.storage.blob.BlobServiceClientBuilder
import com.azure.storage.blob.models.BlobItem
import com.azure.storage.blob.models.BlobItemProperties
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.google.common.net.HttpHeaders
import com.microsoft.azure.functions.HttpStatus
import gov.cdc.prime.router.ActionLog
Expand All @@ -23,17 +32,20 @@ import gov.cdc.prime.router.SubmissionReceiver
import gov.cdc.prime.router.Topic
import gov.cdc.prime.router.TopicReceiver
import gov.cdc.prime.router.UniversalPipelineSender
import gov.cdc.prime.router.azure.BlobAccess.BlobContainerMetadata
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.ktor.utils.io.core.toByteArray
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkClass
import io.mockk.mockkConstructor
import io.mockk.mockkObject
import io.mockk.spyk
import io.mockk.verify
Expand Down Expand Up @@ -172,6 +184,11 @@ class ReportFunctionTests {
"05D2222542&ISO||445297001^Swab of internal nose^SCT^^^^2.67||||53342003^Internal nose structure" +
" (body structure)^SCT^^^^2020-09-01|||||||||202108020000-0500|20210802000006.0000-0500"

@Suppress("ktlint:standard:max-line-length")
val fhirReport = """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347",
|"meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}
""".trimMargin()

private fun makeEngine(metadata: Metadata, settings: SettingsProvider): WorkflowEngine = spyk(
WorkflowEngine.Builder().metadata(metadata).settingsProvider(settings).databaseAccess(accessSpy)
.blobAccess(blobMock).queueAccess(queueMock).hl7Serializer(serializer).build()
Expand Down Expand Up @@ -730,4 +747,81 @@ class ReportFunctionTests {
reportFunc.extractPayloadName(mockHttpRequest)
}
}

@Test
fun `Test Bank Message Retrieval - Unauthorized `() {
val result = ReportFunction().getMessagesFromTestBank(MockHttpRequestMessage())
assert(result.status == HttpStatus.UNAUTHORIZED)
}

@Test
fun `Test Bank Message Retrieval - Authorized, but no reports`() {
mockkClass(BlobAccess::class)
mockkObject(BlobAccess.Companion)
every { BlobAccess.Companion.getBlobConnection(any()) } returns "testconnection"
mockkConstructor(BlobServiceClientBuilder::class)
every { anyConstructed<BlobServiceClientBuilder>().connectionString(any()) } answers
{ BlobServiceClientBuilder() }
every { anyConstructed<BlobServiceClientBuilder>().buildClient() } returns mockk()
val testBlobMetadata = BlobContainerMetadata.build("testcontainer", "test")
every { BlobAccess.Companion.listBlobs("", any()) } returns listOf()
every { BlobAccess.Companion.getBlobContainer(any()) } returns mockk()

val result = ReportFunction().processGetMessageFromTestBankRequest(
MockHttpRequestMessage(),
BlobAccess.Companion,
testBlobMetadata
)
assert(result.status == HttpStatus.OK)
assert(result.body == "[]")
}

@Test
fun `Test Bank Message Retrieval - Authorized, reports`() {
mockkClass(BlobAccess::class)
mockkObject(BlobAccess.Companion)
every { BlobAccess.Companion.getBlobConnection(any()) } returns "testconnection"
mockkConstructor(BlobServiceClientBuilder::class)
every { anyConstructed<BlobServiceClientBuilder>().connectionString(any()) } answers
{ BlobServiceClientBuilder() }
every { anyConstructed<BlobServiceClientBuilder>().buildClient() } returns mockk()
val testBlobMetadata = BlobContainerMetadata.build("testcontainer", "test")
val mockedBlobClient = mockkClass(BlobClient::class)
val mockedBlobContainerClient = mockkClass(BlobContainerClient::class)
every { mockedBlobContainerClient.getBlobClient(any()) } returns mockedBlobClient
every { BlobAccess.Companion.getBlobContainer(any()) } returns mockedBlobContainerClient

val dateCreated = OffsetDateTime.now()
val fileName = UUID.randomUUID().toString() + ".fhir"
val blob1 = BlobItem()
blob1.name = fileName
blob1.properties = BlobItemProperties()
blob1.properties.creationTime = dateCreated

every { mockedBlobClient.downloadContent() } returns BinaryData.fromBytes(fhirReport.toByteArray())

every { BlobAccess.Companion.listBlobs("", any()) } returns listOf(
BlobAccess.Companion.BlobItemAndPreviousVersions(
currentBlobItem = blob1, null
)
)
val result = ReportFunction().processGetMessageFromTestBankRequest(
MockHttpRequestMessage(),
BlobAccess.Companion,
testBlobMetadata
)
assert(result.status == HttpStatus.OK)
val mapper: ObjectMapper = JsonMapper.builder()
.addModule(JavaTimeModule())
.build()
assert(
result.body == "[" + mapper.writeValueAsString(
ReportFunction.TestReportInfo(
dateCreated.toString(),
fileName,
fhirReport
)
) + "]"
)
}
}
Loading