mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-04-19 17:26:51 +00:00
feat: support multiple request content types (#109)
This commit is contained in:
parent
8e3de6a220
commit
c6b37fda69
@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import dev.dnpm.etl.processor.output.BwhcV1MtbFileRequest
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
@ -91,7 +92,7 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
fun mtbFileIsTransformed() {
|
fun mtbFileIsTransformed() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(RequestStatus.SUCCESS)
|
MtbFileSender.Response(RequestStatus.SUCCESS)
|
||||||
}.whenever(mtbFileSender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(mtbFileSender).send(any<BwhcV1MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -134,9 +135,9 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val captor = argumentCaptor<MtbFileSender.MtbFileRequest>()
|
val captor = argumentCaptor<BwhcV1MtbFileRequest>()
|
||||||
verify(mtbFileSender).send(captor.capture())
|
verify(mtbFileSender).send(captor.capture())
|
||||||
assertThat(captor.firstValue.mtbFile.diagnoses).hasSize(1).allMatch { diagnosis ->
|
assertThat(captor.firstValue.content.diagnoses).hasSize(1).allMatch { diagnosis ->
|
||||||
diagnosis.icd10.version == "2014"
|
diagnosis.icd10.version == "2014"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -104,7 +104,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -117,7 +117,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any())
|
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -130,7 +130,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isForbidden() }
|
status { isForbidden() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any())
|
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -177,7 +177,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -190,7 +190,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@ import dev.dnpm.etl.processor.PatientId
|
|||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
@ -47,10 +46,10 @@ class MtbFileRestController(
|
|||||||
@PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
|
@PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
|
||||||
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
||||||
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
||||||
logger.debug("Accepted MTB File for processing")
|
logger.debug("Accepted MTB File (bwHC V1) for processing")
|
||||||
requestProcessor.processMtbFile(mtbFile)
|
requestProcessor.processMtbFile(mtbFile)
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Accepted MTB File and process deletion")
|
logger.debug("Accepted MTB File (bwHC V1) and process deletion")
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
requestProcessor.processDeletion(patientId)
|
requestProcessor.processDeletion(patientId)
|
||||||
}
|
}
|
||||||
@ -59,7 +58,9 @@ class MtbFileRestController(
|
|||||||
|
|
||||||
@PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] )
|
@PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] )
|
||||||
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
|
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build()
|
logger.debug("Accepted MTB File (DNPM V2) for processing")
|
||||||
|
requestProcessor.processMtbFile(mtbFile)
|
||||||
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping(path = ["{patientId}"])
|
@DeleteMapping(path = ["{patientId}"])
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -22,10 +22,12 @@ package dev.dnpm.etl.processor.output
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import org.apache.kafka.clients.producer.ProducerRecord
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.kafka.core.KafkaTemplate
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
|
|
||||||
@ -38,14 +40,20 @@ class KafkaMtbFileSender(
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val result = kafkaTemplate.send(
|
val record =
|
||||||
kafkaProperties.outputTopic,
|
ProducerRecord(kafkaProperties.outputTopic, key(request), objectMapper.writeValueAsString(request))
|
||||||
key(request),
|
when (request) {
|
||||||
objectMapper.writeValueAsString(Data(request.requestId, request.mtbFile))
|
is BwhcV1MtbFileRequest -> record.headers()
|
||||||
)
|
.add("contentType", MediaType.APPLICATION_JSON_VALUE.toByteArray())
|
||||||
|
|
||||||
|
is DnpmV2MtbFileRequest -> record.headers()
|
||||||
|
.add("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = kafkaTemplate.send(record)
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent file via KafkaMtbFileSender")
|
logger.debug("Sent file via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@ -59,7 +67,7 @@ class KafkaMtbFileSender(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
||||||
val dummyMtbFile = MtbFile.builder()
|
val dummyMtbFile = MtbFile.builder()
|
||||||
.withConsent(
|
.withConsent(
|
||||||
Consent.builder()
|
Consent.builder()
|
||||||
@ -71,12 +79,15 @@ class KafkaMtbFileSender(
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val result = kafkaTemplate.send(
|
val record =
|
||||||
|
ProducerRecord(
|
||||||
kafkaProperties.outputTopic,
|
kafkaProperties.outputTopic,
|
||||||
key(request),
|
key(request),
|
||||||
objectMapper.writeValueAsString(Data(request.requestId, dummyMtbFile))
|
// Always use old BwhcV1FileRequest with Consent REJECT
|
||||||
|
objectMapper.writeValueAsString(BwhcV1MtbFileRequest(request.requestId, dummyMtbFile))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val result = kafkaTemplate.send(record)
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@ -94,13 +105,12 @@ class KafkaMtbFileSender(
|
|||||||
return "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})"
|
return "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun key(request: MtbFileSender.MtbFileRequest): String {
|
private fun key(request: MtbRequest): String {
|
||||||
return "{\"pid\": \"${request.mtbFile.patient.id}\"}"
|
return when (request) {
|
||||||
|
is BwhcV1MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}"
|
||||||
|
is DnpmV2MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}"
|
||||||
|
is DeleteRequest -> "{\"pid\": \"${request.patientId.value}\"}"
|
||||||
|
else -> throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun key(request: MtbFileSender.DeleteRequest): String {
|
|
||||||
return "{\"pid\": \"${request.patientId.value}\"}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Data(val requestId: RequestId, val content: MtbFile)
|
|
||||||
}
|
}
|
@ -19,25 +19,17 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.springframework.http.HttpStatusCode
|
import org.springframework.http.HttpStatusCode
|
||||||
|
|
||||||
interface MtbFileSender {
|
interface MtbFileSender {
|
||||||
fun send(request: MtbFileRequest): Response
|
fun <T> send(request: MtbFileRequest<T>): Response
|
||||||
|
|
||||||
fun send(request: DeleteRequest): Response
|
fun send(request: DeleteRequest): Response
|
||||||
|
|
||||||
fun endpoint(): String
|
fun endpoint(): String
|
||||||
|
|
||||||
data class Response(val status: RequestStatus, val body: String = "")
|
data class Response(val status: RequestStatus, val body: String = "")
|
||||||
|
|
||||||
data class MtbFileRequest(val requestId: RequestId, val mtbFile: MtbFile)
|
|
||||||
|
|
||||||
data class DeleteRequest(val requestId: RequestId, val patientId: PatientPseudonym)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.asRequestStatus(): RequestStatus {
|
fun Int.asRequestStatus(): RequestStatus {
|
||||||
|
59
src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt
Normal file
59
src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.RequestId
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
|
|
||||||
|
interface MtbRequest {
|
||||||
|
val requestId: RequestId
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface MtbFileRequest<out T> : MtbRequest {
|
||||||
|
override val requestId: RequestId
|
||||||
|
val content: T
|
||||||
|
|
||||||
|
fun patientPseudonym(): PatientPseudonym
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BwhcV1MtbFileRequest(
|
||||||
|
override val requestId: RequestId,
|
||||||
|
override val content: MtbFile
|
||||||
|
) : MtbFileRequest<MtbFile> {
|
||||||
|
override fun patientPseudonym(): PatientPseudonym {
|
||||||
|
return PatientPseudonym(content.patient.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DnpmV2MtbFileRequest(
|
||||||
|
override val requestId: RequestId,
|
||||||
|
override val content: Mtb
|
||||||
|
) : MtbFileRequest<Mtb> {
|
||||||
|
override fun patientPseudonym(): PatientPseudonym {
|
||||||
|
return PatientPseudonym(content.patient.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeleteRequest(
|
||||||
|
override val requestId: RequestId,
|
||||||
|
val patientId: PatientPseudonym
|
||||||
|
) : MtbRequest
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,10 +19,11 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
@ -46,11 +47,11 @@ abstract class RestMtbFileSender(
|
|||||||
|
|
||||||
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders()
|
val headers = getHttpHeaders(request)
|
||||||
val entityReq = HttpEntity(request.mtbFile, headers)
|
val entityReq = HttpEntity(request.content, headers)
|
||||||
val response = restTemplate.postForEntity(
|
val response = restTemplate.postForEntity(
|
||||||
sendUrl(),
|
sendUrl(),
|
||||||
entityReq,
|
entityReq,
|
||||||
@ -76,10 +77,10 @@ abstract class RestMtbFileSender(
|
|||||||
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders()
|
val headers = getHttpHeaders(request)
|
||||||
val entityReq = HttpEntity(null, headers)
|
val entityReq = HttpEntity(null, headers)
|
||||||
restTemplate.delete(
|
restTemplate.delete(
|
||||||
deleteUrl(request.patientId),
|
deleteUrl(request.patientId),
|
||||||
@ -102,11 +103,15 @@ abstract class RestMtbFileSender(
|
|||||||
return this.restTargetProperties.uri.orEmpty()
|
return this.restTargetProperties.uri.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHttpHeaders(): HttpHeaders {
|
private fun getHttpHeaders(request: MtbRequest): HttpHeaders {
|
||||||
val username = restTargetProperties.username
|
val username = restTargetProperties.username
|
||||||
val password = restTargetProperties.password
|
val password = restTargetProperties.password
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
headers.contentType = when (request) {
|
||||||
|
is BwhcV1MtbFileRequest -> MediaType.APPLICATION_JSON
|
||||||
|
is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
||||||
|
else -> MediaType.APPLICATION_JSON
|
||||||
|
}
|
||||||
|
|
||||||
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
||||||
return headers
|
return headers
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -27,10 +27,11 @@ import dev.dnpm.etl.processor.monitoring.Report
|
|||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.*
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith
|
import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith
|
||||||
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
|
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import org.apache.commons.codec.binary.Base32
|
import org.apache.commons.codec.binary.Base32
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
@ -55,29 +56,40 @@ class RequestProcessor(
|
|||||||
|
|
||||||
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
val pid = PatientId(mtbFile.patient.id)
|
||||||
|
|
||||||
mtbFile pseudonymizeWith pseudonymizeService
|
mtbFile pseudonymizeWith pseudonymizeService
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
mtbFile anonymizeContentWith pseudonymizeService
|
||||||
|
val request = BwhcV1MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
||||||
|
saveAndSend(request, pid)
|
||||||
|
}
|
||||||
|
|
||||||
val request = MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
fun processMtbFile(mtbFile: Mtb) {
|
||||||
|
processMtbFile(mtbFile, randomRequestId())
|
||||||
|
}
|
||||||
|
|
||||||
val patientPseudonym = PatientPseudonym(request.mtbFile.patient.id)
|
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
||||||
|
val pid = PatientId(mtbFile.patient.id)
|
||||||
|
mtbFile pseudonymizeWith pseudonymizeService
|
||||||
|
mtbFile anonymizeContentWith pseudonymizeService
|
||||||
|
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
||||||
|
saveAndSend(request, pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
requestId,
|
request.requestId,
|
||||||
patientPseudonym,
|
request.patientPseudonym(),
|
||||||
pid,
|
pid,
|
||||||
fingerprint(request.mtbFile),
|
fingerprint(request),
|
||||||
RequestType.MTB_FILE,
|
RequestType.MTB_FILE,
|
||||||
RequestStatus.UNKNOWN
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (appConfigProperties.duplicationDetection && isDuplication(mtbFile)) {
|
if (appConfigProperties.duplicationDetection && isDuplication(request)) {
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
requestId,
|
request.requestId,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
RequestStatus.DUPLICATION
|
RequestStatus.DUPLICATION
|
||||||
)
|
)
|
||||||
@ -89,7 +101,7 @@ class RequestProcessor(
|
|||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
requestId,
|
request.requestId,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
responseStatus.status,
|
responseStatus.status,
|
||||||
when (responseStatus.status) {
|
when (responseStatus.status) {
|
||||||
@ -100,8 +112,11 @@ class RequestProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDuplication(pseudonymizedMtbFile: MtbFile): Boolean {
|
private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean {
|
||||||
val patientPseudonym = PatientPseudonym(pseudonymizedMtbFile.patient.id)
|
val patientPseudonym = when (pseudonymizedMtbFileRequest) {
|
||||||
|
is BwhcV1MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
||||||
|
is DnpmV2MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
||||||
|
}
|
||||||
|
|
||||||
val lastMtbFileRequestForPatient =
|
val lastMtbFileRequestForPatient =
|
||||||
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
||||||
@ -109,7 +124,7 @@ class RequestProcessor(
|
|||||||
|
|
||||||
return null != lastMtbFileRequestForPatient
|
return null != lastMtbFileRequestForPatient
|
||||||
&& !isLastRequestDeletion
|
&& !isLastRequestDeletion
|
||||||
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFile)
|
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId) {
|
fun processDeletion(patientId: PatientId) {
|
||||||
@ -131,7 +146,7 @@ class RequestProcessor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val responseStatus = sender.send(MtbFileSender.DeleteRequest(requestId, patientPseudonym))
|
val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym))
|
||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
@ -160,8 +175,11 @@ class RequestProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fingerprint(mtbFile: MtbFile): Fingerprint {
|
private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint {
|
||||||
return fingerprint(objectMapper.writeValueAsString(mtbFile))
|
return when (request) {
|
||||||
|
is BwhcV1MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content))
|
||||||
|
is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fingerprint(s: String): Fingerprint {
|
private fun fingerprint(s: String): Fingerprint {
|
||||||
|
@ -23,10 +23,21 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import com.jayway.jsonpath.JsonPath
|
import com.jayway.jsonpath.JsonPath
|
||||||
import com.jayway.jsonpath.PathNotFoundException
|
import com.jayway.jsonpath.PathNotFoundException
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
|
|
||||||
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
||||||
fun transform(mtbFile: MtbFile): MtbFile {
|
fun transform(mtbFile: MtbFile): MtbFile {
|
||||||
var json = objectMapper.writeValueAsString(mtbFile)
|
val json = transform(objectMapper.writeValueAsString(mtbFile))
|
||||||
|
return objectMapper.readValue(json, MtbFile::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transform(mtbFile: Mtb): Mtb {
|
||||||
|
val json = transform(objectMapper.writeValueAsString(mtbFile))
|
||||||
|
return objectMapper.readValue(json, Mtb::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transform(content: String): String {
|
||||||
|
var json = content
|
||||||
|
|
||||||
transformations.forEach { transformation ->
|
transformations.forEach { transformation ->
|
||||||
val jsonPath = JsonPath.parse(json)
|
val jsonPath = JsonPath.parse(json)
|
||||||
@ -48,7 +59,7 @@ class TransformationService(private val objectMapper: ObjectMapper, private val
|
|||||||
json = jsonPath.jsonString()
|
json = jsonPath.jsonString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(json, MtbFile::class.java)
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTransformations(): List<Transformation> {
|
fun getTransformations(): List<Transformation> {
|
||||||
|
@ -74,7 +74,7 @@ class KafkaInputListenerTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -121,7 +121,7 @@ class KafkaInputListenerTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any(), anyValueClass())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>(), anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.Mtb
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -72,7 +73,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -128,7 +129,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -182,11 +183,11 @@ class MtbFileRestControllerTest {
|
|||||||
contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
isNotImplemented()
|
isAccepted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(0)).processMtbFile(any())
|
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -21,20 +21,25 @@ package dev.dnpm.etl.processor.output
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
|
import de.ukw.ccc.bwhc.dto.Patient
|
||||||
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
|
import org.apache.kafka.clients.producer.ProducerRecord
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.*
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.kafka.core.KafkaTemplate
|
||||||
import org.springframework.kafka.support.SendResult
|
import org.springframework.kafka.support.SendResult
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
@ -45,6 +50,9 @@ import java.util.concurrent.ExecutionException
|
|||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class KafkaMtbFileSenderTest {
|
class KafkaMtbFileSenderTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class BwhcV1Record {
|
||||||
|
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
|
||||||
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
||||||
@ -65,67 +73,69 @@ class KafkaMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("requestWithResponseSource")
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
if (null != testData.exception) {
|
if (null != testData.exception) {
|
||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
val response = kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
val response = kafkaMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("requestWithResponseSource")
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) {
|
fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
if (null != testData.exception) {
|
if (null != testData.exception) {
|
||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
val response = kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldSendMtbFileRequestWithCorrectKeyAndBody() {
|
fun shouldSendMtbFileRequestWithCorrectKeyAndHeaderAndBody() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
kafkaMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
||||||
|
|
||||||
val captor = argumentCaptor<String>()
|
val captor = argumentCaptor<ProducerRecord<String, String>>()
|
||||||
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
verify(kafkaTemplate, times(1)).send(captor.capture())
|
||||||
assertThat(captor.firstValue).isNotNull
|
assertThat(captor.firstValue.key()).isNotNull
|
||||||
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
assertThat(captor.secondValue).isNotNull
|
assertThat(captor.firstValue.headers().headers("contentType")).isNotNull
|
||||||
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
|
assertThat(captor.firstValue.headers().headers("contentType")?.firstOrNull()?.value()).isEqualTo(MediaType.APPLICATION_JSON_VALUE.toByteArray())
|
||||||
|
assertThat(captor.firstValue.value()).isNotNull
|
||||||
|
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(bwhcV1kafkaRecordData(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
|
||||||
val captor = argumentCaptor<String>()
|
val captor = argumentCaptor<ProducerRecord<String, String>>()
|
||||||
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
verify(kafkaTemplate, times(1)).send(captor.capture())
|
||||||
assertThat(captor.firstValue).isNotNull
|
assertThat(captor.firstValue.key()).isNotNull
|
||||||
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
assertThat(captor.secondValue).isNotNull
|
assertThat(captor.firstValue.value()).isNotNull
|
||||||
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.REJECTED)))
|
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(bwhcV1kafkaRecordData(TEST_REQUEST_ID, Consent.Status.REJECTED)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("requestWithResponseSource")
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
@ -136,9 +146,9 @@ class KafkaMtbFileSenderTest {
|
|||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
kafkaMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
||||||
|
|
||||||
val expectedCount = when (testData.exception) {
|
val expectedCount = when (testData.exception) {
|
||||||
// OK - No Retry
|
// OK - No Retry
|
||||||
@ -147,11 +157,11 @@ class KafkaMtbFileSenderTest {
|
|||||||
else -> times(3)
|
else -> times(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("requestWithResponseSource")
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
fun shouldRetryOnDeleteKafkaSendError(testData: TestData) {
|
fun shouldRetryOnDeleteKafkaSendError(testData: TestData) {
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
@ -162,9 +172,9 @@ class KafkaMtbFileSenderTest {
|
|||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
|
||||||
val expectedCount = when (testData.exception) {
|
val expectedCount = when (testData.exception) {
|
||||||
// OK - No Retry
|
// OK - No Retry
|
||||||
@ -173,14 +183,98 @@ class KafkaMtbFileSenderTest {
|
|||||||
else -> times(3)
|
else -> times(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class DnpmV2Record {
|
||||||
|
|
||||||
|
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
|
||||||
|
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
||||||
|
|
||||||
|
private lateinit var objectMapper: ObjectMapper
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup(
|
||||||
|
@Mock kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
) {
|
||||||
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
|
this.objectMapper = ObjectMapper()
|
||||||
|
this.kafkaTemplate = kafkaTemplate
|
||||||
|
|
||||||
|
this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
|
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
||||||
|
doAnswer {
|
||||||
|
if (null != testData.exception) {
|
||||||
|
throw testData.exception
|
||||||
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
|
val response = kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
||||||
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldSendMtbFileRequestWithCorrectKeyAndHeaderAndBody() {
|
||||||
|
doAnswer {
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
||||||
|
|
||||||
|
val captor = argumentCaptor<ProducerRecord<String, String>>()
|
||||||
|
verify(kafkaTemplate, times(1)).send(captor.capture())
|
||||||
|
assertThat(captor.firstValue.key()).isNotNull
|
||||||
|
assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
|
assertThat(captor.firstValue.headers().headers("contentType")).isNotNull
|
||||||
|
assertThat(captor.firstValue.headers().headers("contentType")?.firstOrNull()?.value()).isEqualTo(CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
||||||
|
assertThat(captor.firstValue.value()).isNotNull
|
||||||
|
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(dnmpV2kafkaRecordData(TEST_REQUEST_ID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
||||||
|
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
||||||
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
|
this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper)
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
if (null != testData.exception) {
|
||||||
|
throw testData.exception
|
||||||
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
||||||
|
|
||||||
|
val expectedCount = when (testData.exception) {
|
||||||
|
// OK - No Retry
|
||||||
|
null -> times(1)
|
||||||
|
// Request failed - Retry max 3 times
|
||||||
|
else -> times(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
fun mtbFile(consentStatus: Consent.Status): MtbFile {
|
fun bwhcV1MtbFile(consentStatus: Consent.Status): MtbFile {
|
||||||
return if (consentStatus == Consent.Status.ACTIVE) {
|
return if (consentStatus == Consent.Status.ACTIVE) {
|
||||||
MtbFile.builder()
|
MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -215,8 +309,31 @@ class KafkaMtbFileSenderTest {
|
|||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): KafkaMtbFileSender.Data {
|
fun dnpmV2MtbFile(): Mtb = Mtb.builder()
|
||||||
return KafkaMtbFileSender.Data(requestId, mtbFile(consentStatus))
|
.withPatient(
|
||||||
|
dev.pcvolkmer.mv64e.mtb.Patient.builder()
|
||||||
|
.withId("PID")
|
||||||
|
.withBirthDate("2000-08-08")
|
||||||
|
.withGender(CodingGender.builder().withCode(CodingGender.Code.MALE).build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withEpisodesOfCare(
|
||||||
|
listOf(
|
||||||
|
MTBEpisodeOfCare.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withPatient(Reference("PID"))
|
||||||
|
.withPeriod(PeriodDate.builder().withStart("2023-08-08").build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun bwhcV1kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): MtbRequest {
|
||||||
|
return BwhcV1MtbFileRequest(requestId, bwhcV1MtbFile(consentStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dnmpV2kafkaRecordData(requestId: RequestId): MtbRequest {
|
||||||
|
return DnpmV2MtbFileRequest(requestId, dnpmV2MtbFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
||||||
|
@ -30,16 +30,16 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.Arguments
|
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
import org.springframework.test.web.client.ExpectedCount
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -84,11 +84,12 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
this.mockRestServiceServer
|
this.mockRestServiceServer
|
||||||
.expect(method(HttpMethod.POST))
|
.expect(method(HttpMethod.POST))
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
||||||
|
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||||
.andRespond {
|
.andRespond {
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -118,7 +119,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -148,7 +149,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,8 @@ package dev.dnpm.etl.processor.output
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
|
import de.ukw.ccc.bwhc.dto.Patient
|
||||||
|
import dev.dnpm.etl.processor.CustomMediaType
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
@ -29,25 +31,30 @@ import dev.dnpm.etl.processor.config.AppConfiguration
|
|||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.output.RestBwhcMtbFileSenderTest.Companion
|
import dev.pcvolkmer.mv64e.mtb.*
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.retry.backoff.NoBackOffPolicy
|
import org.springframework.retry.backoff.NoBackOffPolicy
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
import org.springframework.test.web.client.ExpectedCount
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
class RestDipMtbFileSenderTest {
|
class RestDipMtbFileSenderTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class BwhcV1ContentRequest {
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
@ -62,41 +69,28 @@ class RestDipMtbFileSenderTest {
|
|||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
|
|
||||||
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
this.restMtbFileSender =
|
||||||
|
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("deleteRequestWithResponseSource")
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
||||||
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.DELETE))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
||||||
this.mockRestServiceServer
|
this.mockRestServiceServer
|
||||||
.expect(method(HttpMethod.POST))
|
.expect(method(HttpMethod.POST))
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
||||||
|
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
||||||
.andRespond {
|
.andRespond {
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
||||||
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
val restTemplate = RestTemplate()
|
val restTemplate = RestTemplate()
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
||||||
@ -123,13 +117,89 @@ class RestDipMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1mtbFile))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class DnpmV2ContentRequest {
|
||||||
|
|
||||||
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
|
|
||||||
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
|
|
||||||
|
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
val restTemplate = RestTemplate()
|
||||||
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
|
|
||||||
|
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
||||||
|
fun shouldReturnExpectedResponseForDnpmV2MtbFilePost(requestWithResponse: RequestWithResponse) {
|
||||||
|
this.mockRestServiceServer
|
||||||
|
.expect(method(HttpMethod.POST))
|
||||||
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
||||||
|
.andExpect(header(HttpHeaders.CONTENT_TYPE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE))
|
||||||
|
.andRespond {
|
||||||
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = restMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class DeleteRequest {
|
||||||
|
|
||||||
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
|
|
||||||
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
|
|
||||||
|
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
val restTemplate = RestTemplate()
|
||||||
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
|
|
||||||
|
this.restMtbFileSender =
|
||||||
|
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
||||||
|
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
||||||
|
this.mockRestServiceServer
|
||||||
|
.expect(method(HttpMethod.DELETE))
|
||||||
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
||||||
|
.andRespond {
|
||||||
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("deleteRequestWithResponseSource")
|
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
||||||
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
val restTemplate = RestTemplate()
|
val restTemplate = RestTemplate()
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
||||||
@ -156,11 +226,13 @@ class RestDipMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
data class RequestWithResponse(
|
data class RequestWithResponse(
|
||||||
val httpStatus: HttpStatus,
|
val httpStatus: HttpStatus,
|
||||||
@ -171,7 +243,7 @@ class RestDipMtbFileSenderTest {
|
|||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
val mtbFile: MtbFile = MtbFile.builder()
|
val bwhcV1mtbFile: MtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("PID")
|
.withId("PID")
|
||||||
@ -195,6 +267,25 @@ class RestDipMtbFileSenderTest {
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
val dnpmV2MtbFile: Mtb = Mtb.builder()
|
||||||
|
.withPatient(
|
||||||
|
dev.pcvolkmer.mv64e.mtb.Patient.builder()
|
||||||
|
.withId("PID")
|
||||||
|
.withBirthDate("2000-08-08")
|
||||||
|
.withGender(CodingGender.builder().withCode(CodingGender.Code.MALE).build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withEpisodesOfCare(
|
||||||
|
listOf(
|
||||||
|
MTBEpisodeOfCare.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withPatient(Reference("PID"))
|
||||||
|
.withPeriod(PeriodDate.builder().withStart("2023-08-08").build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,14 +21,19 @@ package dev.dnpm.etl.processor.services
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.Fingerprint
|
||||||
|
import dev.dnpm.etl.processor.PatientId
|
||||||
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
|
import dev.dnpm.etl.processor.output.BwhcV1MtbFileRequest
|
||||||
|
import dev.dnpm.etl.processor.output.DeleteRequest
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
|
import dev.dnpm.etl.processor.randomRequestId
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -109,7 +114,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<MtbFile>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -168,7 +173,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<MtbFile>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -223,7 +228,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@ -231,7 +236,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<MtbFile>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -286,7 +291,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@ -294,7 +299,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<MtbFile>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -336,7 +341,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
||||||
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
@ -354,7 +359,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
@ -372,7 +377,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
}.whenever(sender).send(any<DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
@ -404,11 +409,11 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any())
|
}.whenever(transformationService).transform(any<MtbFile>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user