diff options
| author | Paul-Christian Volkmer | 2026-03-11 14:13:29 +0100 |
|---|---|---|
| committer | GitHub | 2026-03-11 13:13:29 +0000 |
| commit | a8f8d5f137c9776a20b2bc91cd3bdd99c9b96991 (patch) | |
| tree | b2df9c483c2324a4bc837b9c4fa127da8fd0333f | |
| parent | 5178673955a69b14ff39bf8a2a73d50ef2fd9cd2 (diff) | |
feat: save error request for invalid input (#264)
5 files changed, 1325 insertions, 1265 deletions
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt index 4c7de9c..ed9d910 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -29,8 +29,6 @@ import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.* -import java.time.Instant -import java.util.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.extension.ExtendWith @@ -51,6 +49,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.post +import java.time.Instant +import java.util.* @WebMvcTest(controllers = [MtbFileRestController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @@ -74,192 +74,81 @@ import org.springframework.test.web.servlet.post ) class MtbFileRestControllerTest { - lateinit var mockMvc: MockMvc - lateinit var requestProcessor: RequestProcessor - lateinit var consentEvaluator: ConsentEvaluator - - @BeforeEach - fun setup( - @Autowired mockMvc: MockMvc, - @Autowired requestProcessor: RequestProcessor, - @Autowired consentEvaluator: ConsentEvaluator, - ) { - this.mockMvc = mockMvc - this.requestProcessor = requestProcessor - this.consentEvaluator = consentEvaluator - - doAnswer { ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true) } - .whenever(consentEvaluator) - .check(any()) - } - - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile", - "/mtbfile/etl/patient-record", - "/mtb", - "/mtb/etl/patient-record", - "/api/mtbfile", - "/api/mtbfile/etl/patient-record", - "/api/mtb", - "/api/mtb/etl/patient-record", - ] - ) - fun testShouldGrantPermissionToSendMtbFile(url: String) { - mockMvc - .post(url) { - with(user("onkostarserver").roles("MTBFILE")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - } - .andExpect { status { isAccepted() } } - - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } - - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile", - "/mtbfile/etl/patient-record", - "/mtb", - "/mtb/etl/patient-record", - "/api/mtbfile", - "/api/mtbfile/etl/patient-record", - "/api/mtb", - "/api/mtb/etl/patient-record", - ] - ) - fun testShouldGrantPermissionToSendMtbFileToAdminUser(url: String) { - mockMvc - .post(url) { - with(user("onkostarserver").roles("ADMIN")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - } - .andExpect { status { isAccepted() } } + lateinit var mockMvc: MockMvc + lateinit var requestProcessor: RequestProcessor + lateinit var consentEvaluator: ConsentEvaluator - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } + @BeforeEach + fun setup( + @Autowired mockMvc: MockMvc, + @Autowired requestProcessor: RequestProcessor, + @Autowired consentEvaluator: ConsentEvaluator, + ) { + this.mockMvc = mockMvc + this.requestProcessor = requestProcessor + this.consentEvaluator = consentEvaluator - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile", - "/mtbfile/etl/patient-record", - "/mtb", - "/mtb/etl/patient-record", - "/api/mtbfile", - "/api/mtbfile/etl/patient-record", - "/api/mtb", - "/api/mtb/etl/patient-record", - ] - ) - fun testShouldGrantPermissionToSendMtbFileToUser(url: String) { - mockMvc - .post(url) { - with(user("testuser").roles("USER")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - } - .andExpect { status { isAccepted() } } - - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } + doAnswer { ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true) } + .whenever(consentEvaluator) + .check(any()) + } - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile", - "/mtbfile/etl/patient-record", - "/mtb", - "/mtb/etl/patient-record", - "/api/mtbfile", - "/api/mtbfile/etl/patient-record", - "/api/mtb", - "/api/mtb/etl/patient-record", - ] - ) - fun testShouldDenyPermissionToSendMtbFileForAnonymous(url: String) { - mockMvc - .post(url) { - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - } - .andExpect { status { isUnauthorized() } } + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile", + "/mtbfile/etl/patient-record", + "/mtb", + "/mtb/etl/patient-record", + "/api/mtbfile", + "/api/mtbfile/etl/patient-record", + "/api/mtb", + "/api/mtb/etl/patient-record", + ] + ) + fun testShouldGrantPermissionToSendMtbFile(url: String) { + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) - verify(requestProcessor, never()).processMtbFile(any<Mtb>()) - } + mockMvc + .post(url) { + with(user("onkostarserver").roles("MTBFILE")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isAccepted() } } - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile/TEST_12345678", - "/mtbfile/etl/patient-record/TEST_12345678", - "/mtbfile/etl/patient/TEST_12345678", - "/mtb/TEST_12345678", - "/mtb/etl/patient-record/TEST_12345678", - "/mtb/etl/patient/TEST_12345678", - "/api/mtbfile/TEST_12345678", - "/api/mtbfile/etl/patient-record/TEST_12345678", - "/api/mtbfile/etl/patient/TEST_12345678", - "/api/mtb/TEST_12345678", - "/api/mtb/etl/patient-record/TEST_12345678", - "/api/mtb/etl/patient/TEST_12345678", - ] - ) - fun testShouldGrantPermissionToDeletePatientData(url: String) { - mockMvc - .delete(url) { with(user("onkostarserver").roles("MTBFILE")) } - .andExpect { status { isAccepted() } } + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } - verify(requestProcessor, times(1)) - .processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)) - } + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile", + "/mtbfile/etl/patient-record", + "/mtb", + "/mtb/etl/patient-record", + "/api/mtbfile", + "/api/mtbfile/etl/patient-record", + "/api/mtb", + "/api/mtb/etl/patient-record", + ] + ) + fun testShouldGrantPermissionToSendMtbFileToAdminUser(url: String) { + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile/TEST_12345678", - "/mtbfile/etl/patient-record/TEST_12345678", - "/mtbfile/etl/patient/TEST_12345678", - "/mtb/TEST_12345678", - "/mtb/etl/patient-record/TEST_12345678", - "/mtb/etl/patient/TEST_12345678", - "/api/mtbfile/TEST_12345678", - "/api/mtbfile/etl/patient-record/TEST_12345678", - "/api/mtbfile/etl/patient/TEST_12345678", - "/api/mtb/TEST_12345678", - "/api/mtb/etl/patient-record/TEST_12345678", - "/api/mtb/etl/patient/TEST_12345678", - ] - ) - fun testShouldDenyPermissionToDeletePatientData(url: String) { - mockMvc.delete(url) { with(anonymous()) }.andExpect { status { isUnauthorized() } } + mockMvc + .post(url) { + with(user("onkostarserver").roles("ADMIN")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isAccepted() } } - verify(requestProcessor, never()).processDeletion(anyValueClass(), any()) - } + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } - @Nested - @MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::class]) - @TestPropertySource( - properties = - [ - "app.pseudonymize.generator=BUILDIN", - "app.security.admin-user=admin", - "app.security.admin-password={noop}very-secret", - "app.security.enable-tokens=true", - "app.security.enable-oidc=true", - ] - ) - inner class WithOidcEnabled { @ParameterizedTest @ValueSource( strings = @@ -274,16 +163,18 @@ class MtbFileRestControllerTest { "/api/mtb/etl/patient-record", ] ) - fun testShouldGrantPermissionToSendMtbFileToAdminUser(url: String) { - mockMvc - .post(url) { - with(user("onkostarserver").roles("ADMIN")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - } - .andExpect { status { isAccepted() } } + fun testShouldGrantPermissionToSendMtbFileToUser(url: String) { + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) + + mockMvc + .post(url) { + with(user("testuser").roles("USER")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isAccepted() } } - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) } @ParameterizedTest @@ -300,37 +191,156 @@ class MtbFileRestControllerTest { "/api/mtb/etl/patient-record", ] ) - fun testShouldGrantPermissionToSendMtbFileToUser(url: String) { - mockMvc - .post(url) { - with(user("onkostarserver").roles("USER")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - } - .andExpect { status { isAccepted() } } + fun testShouldDenyPermissionToSendMtbFileForAnonymous(url: String) { + mockMvc + .post(url) { + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isUnauthorized() } } + + verify(requestProcessor, never()).processMtbFile(any<Mtb>()) + } + + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile/TEST_12345678", + "/mtbfile/etl/patient-record/TEST_12345678", + "/mtbfile/etl/patient/TEST_12345678", + "/mtb/TEST_12345678", + "/mtb/etl/patient-record/TEST_12345678", + "/mtb/etl/patient/TEST_12345678", + "/api/mtbfile/TEST_12345678", + "/api/mtbfile/etl/patient-record/TEST_12345678", + "/api/mtbfile/etl/patient/TEST_12345678", + "/api/mtb/TEST_12345678", + "/api/mtb/etl/patient-record/TEST_12345678", + "/api/mtb/etl/patient/TEST_12345678", + ] + ) + fun testShouldGrantPermissionToDeletePatientData(url: String) { + mockMvc + .delete(url) { with(user("onkostarserver").roles("MTBFILE")) } + .andExpect { status { isAccepted() } } + + verify(requestProcessor, times(1)) + .processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)) + } - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile/TEST_12345678", + "/mtbfile/etl/patient-record/TEST_12345678", + "/mtbfile/etl/patient/TEST_12345678", + "/mtb/TEST_12345678", + "/mtb/etl/patient-record/TEST_12345678", + "/mtb/etl/patient/TEST_12345678", + "/api/mtbfile/TEST_12345678", + "/api/mtbfile/etl/patient-record/TEST_12345678", + "/api/mtbfile/etl/patient/TEST_12345678", + "/api/mtb/TEST_12345678", + "/api/mtb/etl/patient-record/TEST_12345678", + "/api/mtb/etl/patient/TEST_12345678", + ] + ) + fun testShouldDenyPermissionToDeletePatientData(url: String) { + mockMvc.delete(url) { with(anonymous()) }.andExpect { status { isUnauthorized() } } + + verify(requestProcessor, never()).processDeletion(anyValueClass(), any()) + } + + @Nested + @MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::class]) + @TestPropertySource( + properties = + [ + "app.pseudonymize.generator=BUILDIN", + "app.security.admin-user=admin", + "app.security.admin-password={noop}very-secret", + "app.security.enable-tokens=true", + "app.security.enable-oidc=true", + ] + ) + inner class WithOidcEnabled { + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile", + "/mtbfile/etl/patient-record", + "/mtb", + "/mtb/etl/patient-record", + "/api/mtbfile", + "/api/mtbfile/etl/patient-record", + "/api/mtb", + "/api/mtb/etl/patient-record", + ] + ) + fun testShouldGrantPermissionToSendMtbFileToAdminUser(url: String) { + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) + + mockMvc + .post(url) { + with(user("onkostarserver").roles("ADMIN")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isAccepted() } } + + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } + + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile", + "/mtbfile/etl/patient-record", + "/mtb", + "/mtb/etl/patient-record", + "/api/mtbfile", + "/api/mtbfile/etl/patient-record", + "/api/mtb", + "/api/mtb/etl/patient-record", + ] + ) + fun testShouldGrantPermissionToSendMtbFileToUser(url: String) { + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) + + mockMvc + .post(url) { + with(user("onkostarserver").roles("USER")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isAccepted() } } + + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } } - } - companion object { + companion object { - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("PID").build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("PID").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))) - .build() - ) - .build() + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("PID").build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("PID").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))) + .build() + ) + .build() + ) ) - ) - .build() - } + .build() + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt index f4ab194..523a0a8 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -33,36 +33,37 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping(path = ["mtbfile", "mtb", "api/mtbfile", "api/mtb"]) class MtbFileRestController( - private val requestProcessor: RequestProcessor, - private val consentEvaluator: ConsentEvaluator, + private val requestProcessor: RequestProcessor ) { - private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) + private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) - @GetMapping - fun info(): ResponseEntity<String> { - return ResponseEntity.ok("Test") - } + @GetMapping + fun info(): ResponseEntity<String> { + return ResponseEntity.ok("Test") + } - @PostMapping( - path = ["", "etl/patient-record"], - consumes = - [ - MediaType.APPLICATION_JSON_VALUE, - CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE, - ], - ) - fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> { - logger.debug("Accepted MTB File (DNPM V2) for processing") - requestProcessor.processMtbFile(mtbFile) - return ResponseEntity.accepted().build() - } + @PostMapping( + path = ["", "etl/patient-record"], + consumes = + [ + MediaType.APPLICATION_JSON_VALUE, + CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE, + ], + ) + fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> { + logger.debug("Accepted MTB File (DNPM V2) for processing") + if (requestProcessor.processMtbFile(mtbFile)) { + return ResponseEntity.accepted().build() + } + return ResponseEntity.badRequest().build() + } - @DeleteMapping( - path = ["{patientId}", "etl/patient-record/{patientId}", "etl/patient/{patientId}"] - ) - fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> { - logger.debug("Accepted patient ID to process deletion") - requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE) - return ResponseEntity.accepted().build() - } + @DeleteMapping( + path = ["{patientId}", "etl/patient-record/{patientId}", "etl/patient/{patientId}"] + ) + fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> { + logger.debug("Accepted patient ID to process deletion") + requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE) + return ResponseEntity.accepted().build() + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt index fe1fd3b..7325265 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -56,225 +56,251 @@ class RequestProcessor( private val consentProcessor: ConsentProcessor?, ) { - private var logger: Logger = LoggerFactory.getLogger("RequestProcessor") + private var logger: Logger = LoggerFactory.getLogger("RequestProcessor") - fun processMtbFile(mtbFile: Mtb) { - processMtbFile(mtbFile, randomRequestId()) - } + fun processMtbFile(mtbFile: Mtb): Boolean { + return processMtbFile(mtbFile, randomRequestId()) + } + + fun processMtbFile(mtbFile: Mtb, requestId: RequestId): Boolean { + val isConsentOk = + consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || + consentProcessor == null - fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { - val isConsentOk = - consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || - consentProcessor == null + if (!isConsentOk) { + logger.warn("consent check failed but will be sent to DNPM:DIP!") + } - if (!isConsentOk) { - logger.warn("consent check failed but will be sent to DNPM:DIP!") + try { + mtbFile addGenomDeTan pseudonymizeService + mtbFile pseudonymizeWith pseudonymizeService + mtbFile anonymizeContentWith pseudonymizeService + val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) + saveAndSend(request) + } catch (e: Exception) { + logger.error("Error while processing MtbFile", e) + requestService.save( + Request( + null, + requestId, + PatientPseudonym("INVALID"), + emptyPatientId(), + fingerprint(""), + RequestType.MTB_FILE, + SubmissionType.UNKNOWN, + RequestStatus.ERROR, + Tan.empty(), + report = Report("Fehlerhafte Eingangsdaten. Keine Verarbeitung oder Weiterleitung."), + ) + ) + return false + } + return true } - mtbFile addGenomDeTan pseudonymizeService - mtbFile pseudonymizeWith pseudonymizeService - mtbFile anonymizeContentWith pseudonymizeService - val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) - saveAndSend(request) - } - - private fun <T> saveAndSend(request: MtbFileRequest<T>) { - var submissionType: SubmissionType = - when (request) { - is DnpmV2MtbFileRequest -> { - when (request.content.metadata?.type) { - MvhSubmissionType.TEST -> SubmissionType.TEST - MvhSubmissionType.INITIAL -> SubmissionType.INITIAL - MvhSubmissionType.ADDITION -> SubmissionType.ADDITION - MvhSubmissionType.CORRECTION -> SubmissionType.CORRECTION - MvhSubmissionType.FOLLOWUP -> SubmissionType.FOLLOWUP - else -> SubmissionType.UNKNOWN + private fun <T> saveAndSend(request: MtbFileRequest<T>) { + var submissionType: SubmissionType = + when (request) { + is DnpmV2MtbFileRequest -> { + when (request.content.metadata?.type) { + MvhSubmissionType.TEST -> SubmissionType.TEST + MvhSubmissionType.INITIAL -> SubmissionType.INITIAL + MvhSubmissionType.ADDITION -> SubmissionType.ADDITION + MvhSubmissionType.CORRECTION -> SubmissionType.CORRECTION + MvhSubmissionType.FOLLOWUP -> SubmissionType.FOLLOWUP + else -> SubmissionType.UNKNOWN + } + } } - } - } - if ( - appConfigProperties.postInitialSubmissionBlock && + if ( + appConfigProperties.postInitialSubmissionBlock && hasSuccessfullInitialSubmission(request.patientPseudonym()) && hasUnacceptedInitialSubmission(request.patientPseudonym()) - ) { - requestService.save( - Request( - request.requestId, - request.patientPseudonym(), - emptyPatientId(), - fingerprint(request), - RequestType.MTB_FILE, - submissionType, - RequestStatus.BLOCKED_INITIAL, - Tan(request.content.metadata?.transferTan.orEmpty()) - ) - ) - // Exit - no further processing - return - } + ) { + requestService.save( + Request( + request.requestId, + request.patientPseudonym(), + emptyPatientId(), + fingerprint(request), + RequestType.MTB_FILE, + submissionType, + RequestStatus.BLOCKED_INITIAL, + Tan(request.content.metadata?.transferTan.orEmpty()) + ) + ) + // Exit - no further processing + return + } - if ( - appConfigProperties.postInitialSubmissionBlock && + if ( + appConfigProperties.postInitialSubmissionBlock && hasSuccessfullInitialSubmission(request.patientPseudonym()) && !hasUnacceptedInitialSubmission(request.patientPseudonym()) - ) { - // Use "addition" after "intial" with "Meldebestaetigung" - request.content.metadata?.let { - logger.warn("Override submission type using 'addition' after first initial submission!") - it.type = MvhSubmissionType.ADDITION - submissionType = SubmissionType.ADDITION - } - } + ) { + // Use "addition" after "intial" with "Meldebestaetigung" + request.content.metadata?.let { + logger.warn("Override submission type using 'addition' after first initial submission!") + it.type = MvhSubmissionType.ADDITION + submissionType = SubmissionType.ADDITION + } + } - requestService.save( - Request( - request.requestId, - request.patientPseudonym(), - emptyPatientId(), - fingerprint(request), - RequestType.MTB_FILE, - submissionType, - RequestStatus.UNKNOWN, - Tan(request.content.metadata?.transferTan.orEmpty()), + requestService.save( + Request( + request.requestId, + request.patientPseudonym(), + emptyPatientId(), + fingerprint(request), + RequestType.MTB_FILE, + submissionType, + RequestStatus.UNKNOWN, + Tan(request.content.metadata?.transferTan.orEmpty()), + ) ) - ) - if (appConfigProperties.duplicationDetection && isDuplication(request)) { - applicationEventPublisher.publishEvent( - ResponseEvent(request.requestId, Instant.now(), RequestStatus.DUPLICATION) - ) - return - } + if (appConfigProperties.duplicationDetection && isDuplication(request)) { + applicationEventPublisher.publishEvent( + ResponseEvent(request.requestId, Instant.now(), RequestStatus.DUPLICATION) + ) + return + } - val responseStatus = sender.send(request) - - applicationEventPublisher.publishEvent( - ResponseEvent( - request.requestId, - Instant.now(), - responseStatus.status, - when (responseStatus.status) { - RequestStatus.ERROR, - RequestStatus.WARNING -> Optional.of(responseStatus.body) - else -> Optional.empty() - }, - ) - ) - } + val responseStatus = sender.send(request) - private fun hasSuccessfullInitialSubmission(patientPseudonym: PatientPseudonym): Boolean { - return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any { - it.submissionType == SubmissionType.INITIAL && - (it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING) + applicationEventPublisher.publishEvent( + ResponseEvent( + request.requestId, + Instant.now(), + responseStatus.status, + when (responseStatus.status) { + RequestStatus.ERROR, + RequestStatus.WARNING -> Optional.of(responseStatus.body) + + else -> Optional.empty() + }, + ) + ) } - } - private fun hasUnacceptedInitialSubmission(patientPseudonym: PatientPseudonym): Boolean { - return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any { - it.submissionType == SubmissionType.INITIAL && - !(it.submissionAccepted || it.status == RequestStatus.BLOCKED_INITIAL) + private fun hasSuccessfullInitialSubmission(patientPseudonym: PatientPseudonym): Boolean { + return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any { + it.submissionType == SubmissionType.INITIAL && + (it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING) + } } - } - private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean { - val patientPseudonym = - when (pseudonymizedMtbFileRequest) { - is DnpmV2MtbFileRequest -> - PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id) + private fun hasUnacceptedInitialSubmission(patientPseudonym: PatientPseudonym): Boolean { + return this.requestService.allRequestsByPatientPseudonym(patientPseudonym).any { + it.submissionType == SubmissionType.INITIAL && + !(it.submissionAccepted || it.status == RequestStatus.BLOCKED_INITIAL) } + } - val lastMtbFileRequestForPatient = - requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) - val isLastRequestDeletion = - requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) - - return null != lastMtbFileRequestForPatient && - !isLastRequestDeletion && - lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest) - } - - fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) { - processDeletion(patientId, randomRequestId(), isConsented) - } - - fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) { - try { - val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) - - val requestStatus: RequestStatus = - when (isConsented) { - TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, - TtpConsentStatus.BROAD_CONSENT_MISSING, - TtpConsentStatus.BROAD_CONSENT_REJECTED -> RequestStatus.NO_CONSENT - TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR - TtpConsentStatus.BROAD_CONSENT_GIVEN, - TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN - TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, - TtpConsentStatus.GENOM_DE_CONSENT_MISSING, - TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED -> { - throw RuntimeException( - "processDelete should never deal with '" + - isConsented.name + - "' consent status. This is a bug and need to be fixed!" - ) + private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean { + val patientPseudonym = + when (pseudonymizedMtbFileRequest) { + is DnpmV2MtbFileRequest -> + PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id) } - } - - requestService.save( - Request( - requestId, - patientPseudonym, - emptyPatientId(), - fingerprint(patientPseudonym.value), - RequestType.DELETE, - SubmissionType.UNKNOWN, - requestStatus, - Tan.empty() - ) - ) - - val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym)) - - applicationEventPublisher.publishEvent( - ResponseEvent( - requestId, - Instant.now(), - responseStatus.status, - when (responseStatus.status) { - RequestStatus.WARNING, - RequestStatus.ERROR -> Optional.of(responseStatus.body) - else -> Optional.empty() - }, - ) - ) - } catch (_: Exception) { - requestService.save( - Request( - uuid = requestId, - patientPseudonym = emptyPatientPseudonym(), - pid = patientId, - fingerprint = Fingerprint.empty(), - status = RequestStatus.ERROR, - type = RequestType.DELETE, - submissionType = SubmissionType.UNKNOWN, - report = Report("Fehler bei der Pseudonymisierung"), - tan = Tan.empty(), - ) - ) + + val lastMtbFileRequestForPatient = + requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) + val isLastRequestDeletion = + requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) + + return null != lastMtbFileRequestForPatient && + !isLastRequestDeletion && + lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest) } - } - private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint { - return when (request) { - is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content)) + fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) { + processDeletion(patientId, randomRequestId(), isConsented) } - } - private fun fingerprint(s: String): Fingerprint { - return Fingerprint(Base32().encodeAsString(DigestUtils.sha256(s)) - .replace("=", "") - .lowercase()) - } + fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) { + try { + val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) + + val requestStatus: RequestStatus = + when (isConsented) { + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, + TtpConsentStatus.BROAD_CONSENT_MISSING, + TtpConsentStatus.BROAD_CONSENT_REJECTED -> RequestStatus.NO_CONSENT + + TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR + TtpConsentStatus.BROAD_CONSENT_GIVEN, + TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN + + TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, + TtpConsentStatus.GENOM_DE_CONSENT_MISSING, + TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED -> { + throw RuntimeException( + "processDelete should never deal with '" + + isConsented.name + + "' consent status. This is a bug and need to be fixed!" + ) + } + } + + requestService.save( + Request( + requestId, + patientPseudonym, + emptyPatientId(), + fingerprint(patientPseudonym.value), + RequestType.DELETE, + SubmissionType.UNKNOWN, + requestStatus, + Tan.empty() + ) + ) + + val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym)) + + applicationEventPublisher.publishEvent( + ResponseEvent( + requestId, + Instant.now(), + responseStatus.status, + when (responseStatus.status) { + RequestStatus.WARNING, + RequestStatus.ERROR -> Optional.of(responseStatus.body) + + else -> Optional.empty() + }, + ) + ) + } catch (_: Exception) { + requestService.save( + Request( + uuid = requestId, + patientPseudonym = emptyPatientPseudonym(), + pid = patientId, + fingerprint = Fingerprint.empty(), + status = RequestStatus.ERROR, + type = RequestType.DELETE, + submissionType = SubmissionType.UNKNOWN, + report = Report("Fehler bei der Pseudonymisierung"), + tan = Tan.empty(), + ) + ) + } + } + + private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint { + return when (request) { + is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content)) + } + } + + private fun fingerprint(s: String): Fingerprint { + return Fingerprint( + Base32().encodeAsString(DigestUtils.sha256(s)) + .replace("=", "") + .lowercase() + ) + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt index f3d669b..c8e5804 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -22,14 +22,11 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper import dev.dnpm.etl.processor.ArgProvider import dev.dnpm.etl.processor.CustomMediaType -import dev.dnpm.etl.processor.consent.ConsentEvaluation import dev.dnpm.etl.processor.consent.ConsentEvaluator import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.input.Dnpm21MtbFile.Companion.buildMtb import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.* -import java.time.Instant -import java.util.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -50,134 +47,148 @@ import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.setup.MockMvcBuilders +import java.time.Instant +import java.util.* @ExtendWith(MockitoExtension::class) class MtbFileRestControllerTest { - private val objectMapper = ObjectMapper() + private val objectMapper = ObjectMapper() - @Nested - inner class RequestsForDnpmDataModel21 { + @Nested + inner class RequestsForDnpmDataModel21 { - private lateinit var mockMvc: MockMvc + private lateinit var mockMvc: MockMvc - private lateinit var requestProcessor: RequestProcessor - private lateinit var consentEvaluator: ConsentEvaluator + private lateinit var requestProcessor: RequestProcessor - @BeforeEach - fun setup(@Mock requestProcessor: RequestProcessor, @Mock consentEvaluator: ConsentEvaluator) { - this.requestProcessor = requestProcessor - this.consentEvaluator = consentEvaluator - val controller = MtbFileRestController(requestProcessor, consentEvaluator) - this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() - } + @BeforeEach + fun setup(@Mock requestProcessor: RequestProcessor) { + this.requestProcessor = requestProcessor + val controller = MtbFileRestController(requestProcessor) + this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } - @Test - fun shouldRespondPostRequest() { - val mtbFileContent = - ClassPathResource("mv64e-mtb-fake-patient.json") - .inputStream - .readAllBytes() - .toString(Charsets.UTF_8) + @Test + fun shouldRespondPostRequest() { + val mtbFileContent = + ClassPathResource("mv64e-mtb-fake-patient.json") + .inputStream + .readAllBytes() + .toString(Charsets.UTF_8) - mockMvc - .post("/mtb") { - content = mtbFileContent - contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON - } - .andExpect { status { isAccepted() } } + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } + mockMvc + .post("/mtb") { + content = mtbFileContent + contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + } + .andExpect { status { isAccepted() } } - @ParameterizedTest - @ArgumentsSource(Dnpm21MtbFile::class) - fun shouldProcessPostRequest(mtb: Mtb) { - mockMvc - .post("/mtbfile") { - content = objectMapper.writeValueAsString(mtb) - contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON - } - .andExpect { status { isAccepted() } } + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } - } + @ParameterizedTest + @ArgumentsSource(Dnpm21MtbFile::class) + fun shouldProcessPostRequest(mtb: Mtb) { + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile", - "/mtbfile/etl/patient-record", - "/mtb", - "/mtb/etl/patient-record", - "/api/mtbfile", - "/api/mtbfile/etl/patient-record", - "/api/mtb", - "/api/mtb/etl/patient-record", - ] - ) - fun shouldAcceptPostRequests(url: String) { - val mtb = - buildMtb( - MvhMetadata.builder() - .modelProjectConsent( - ModelProjectConsent.builder() - .provisions( - listOf( - Provision.builder() - .date(Date()) - .type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.SEQUENCING) - .build() - ) - ) - .build() - ) - .build() - ) + mockMvc + .post("/mtbfile") { + content = objectMapper.writeValueAsString(mtb) + contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + } + .andExpect { status { isAccepted() } } + } - mockMvc - .post(url) { - content = objectMapper.writeValueAsString(mtb) - contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON - } - .andExpect { status { isAccepted() } } - } + @Test + fun shouldNotAcceptInvalidPostRequest() { + mockMvc + .post("/mtbfile") { + content = "{}" + contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + } + .andExpect { status { isBadRequest() } } + } - @Test - fun shouldProcessDeleteRequest() { - mockMvc.delete("/mtbfile/TEST_12345678").andExpect { status { isAccepted() } } + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile", + "/mtbfile/etl/patient-record", + "/mtb", + "/mtb/etl/patient-record", + "/api/mtbfile", + "/api/mtbfile/etl/patient-record", + "/api/mtb", + "/api/mtb/etl/patient-record", + ] + ) + fun shouldAcceptPostRequests(url: String) { + val mtb = + buildMtb( + MvhMetadata.builder() + .modelProjectConsent( + ModelProjectConsent.builder() + .provisions( + listOf( + Provision.builder() + .date(Date()) + .type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() + ) + ) + .build() + ) + .build() + ) - verify(requestProcessor, times(1)) - .processDeletion( - anyValueClass(), - org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE), - ) - verify(consentEvaluator, times(0)).check(any<Mtb>()) - } + whenever { requestProcessor.processMtbFile(any<Mtb>()) }.thenReturn(true) + + mockMvc + .post(url) { + content = objectMapper.writeValueAsString(mtb) + contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + } + .andExpect { status { isAccepted() } } + } + + @Test + fun shouldProcessDeleteRequest() { + mockMvc.delete("/mtbfile/TEST_12345678").andExpect { status { isAccepted() } } - @ParameterizedTest - @ValueSource( - strings = - [ - "/mtbfile/TEST_12345678", - "/mtbfile/etl/patient-record/TEST_12345678", - "/mtbfile/etl/patient/TEST_12345678", - "/mtb/TEST_12345678", - "/mtb/etl/patient-record/TEST_12345678", - "/mtb/etl/patient/TEST_12345678", - "/api/mtbfile/TEST_12345678", - "/api/mtbfile/etl/patient-record/TEST_12345678", - "/api/mtbfile/etl/patient/TEST_12345678", - "/api/mtb/TEST_12345678", - "/api/mtb/etl/patient-record/TEST_12345678", - "/api/mtb/etl/patient/TEST_12345678", - ] - ) - fun shouldAcceptDeleteRequests(url: String) { - mockMvc.delete(url).andExpect { status { isAccepted() } } + verify(requestProcessor, times(1)) + .processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE), + ) + } + + @ParameterizedTest + @ValueSource( + strings = + [ + "/mtbfile/TEST_12345678", + "/mtbfile/etl/patient-record/TEST_12345678", + "/mtbfile/etl/patient/TEST_12345678", + "/mtb/TEST_12345678", + "/mtb/etl/patient-record/TEST_12345678", + "/mtb/etl/patient/TEST_12345678", + "/api/mtbfile/TEST_12345678", + "/api/mtbfile/etl/patient-record/TEST_12345678", + "/api/mtbfile/etl/patient/TEST_12345678", + "/api/mtb/TEST_12345678", + "/api/mtb/etl/patient-record/TEST_12345678", + "/api/mtb/etl/patient/TEST_12345678", + ] + ) + fun shouldAcceptDeleteRequests(url: String) { + mockMvc.delete(url).andExpect { status { isAccepted() } } + } } - } } class Dnpm21MtbFile : @@ -234,26 +245,26 @@ class Dnpm21MtbFile : ), ) { - companion object { - fun buildMtb(metadata: MvhMetadata?): Mtb { - return Mtb.builder() - .patient( - Patient.builder() - .id("TEST_12345678") - .birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))) - .gender(GenderCoding.builder().code(GenderCodingCode.MALE).build()) - .build() - ) - .metadata(metadata) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("TEST_12345678").build()) - .build() - ) - ) - .build() + companion object { + fun buildMtb(metadata: MvhMetadata?): Mtb { + return Mtb.builder() + .patient( + Patient.builder() + .id("TEST_12345678") + .birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))) + .gender(GenderCoding.builder().code(GenderCodingCode.MALE).build()) + .build() + ) + .metadata(metadata) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("TEST_12345678").build()) + .build() + ) + ) + .build() + } } - } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt index afa6872..70df248 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt @@ -20,10 +20,7 @@ package dev.dnpm.etl.processor.services import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.Fingerprint -import dev.dnpm.etl.processor.PatientId -import dev.dnpm.etl.processor.PatientPseudonym -import dev.dnpm.etl.processor.Tan +import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.monitoring.Request @@ -35,10 +32,7 @@ import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest import dev.dnpm.etl.processor.output.MtbFileSender import dev.dnpm.etl.processor.output.RestMtbFileSender import dev.dnpm.etl.processor.pseudonym.PseudonymizeService -import dev.dnpm.etl.processor.randomRequestId import dev.pcvolkmer.mv64e.mtb.* -import java.time.Instant -import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -52,761 +46,779 @@ import org.mockito.kotlin.anyValueClass import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever import org.springframework.context.ApplicationEventPublisher +import java.time.Instant +import java.util.* @ExtendWith(MockitoExtension::class) class RequestProcessorTest { - private lateinit var pseudonymizeService: PseudonymizeService - private lateinit var transformationService: TransformationService - private lateinit var sender: MtbFileSender - private lateinit var requestService: RequestService - private lateinit var applicationEventPublisher: ApplicationEventPublisher - private lateinit var appConfigProperties: AppConfigProperties - private lateinit var consentProcessor: ConsentProcessor - private lateinit var requestProcessor: RequestProcessor - - @BeforeEach - fun setup( - @Mock pseudonymizeService: PseudonymizeService, - @Mock transformationService: TransformationService, - @Mock sender: RestMtbFileSender, - @Mock requestService: RequestService, - @Mock applicationEventPublisher: ApplicationEventPublisher, - @Mock consentProcessor: ConsentProcessor, - ) { - this.pseudonymizeService = pseudonymizeService - this.transformationService = transformationService - this.sender = sender - this.requestService = requestService - this.applicationEventPublisher = applicationEventPublisher - this.appConfigProperties = AppConfigProperties() - this.consentProcessor = consentProcessor - - val objectMapper = ObjectMapper() - - requestProcessor = - RequestProcessor( - pseudonymizeService, - transformationService, - sender, - requestService, - objectMapper, - applicationEventPublisher, - appConfigProperties, - consentProcessor, - ) - } - - @Test - fun testShouldSendMtbFileDuplicationAndSaveUnknownRequestStatusAtFirst() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("6vkiti5bk6ikwifpajpt7cygmd3dvw54d6lwfhzlynb3pqtzferq"), - RequestType.MTB_FILE, - SubmissionType.TEST, - RequestStatus.SUCCESS, - Tan.empty(), - Instant.parse("2023-08-08T02:00:00Z"), - ) - } - .whenever(requestService) - .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + private lateinit var pseudonymizeService: PseudonymizeService + private lateinit var transformationService: TransformationService + private lateinit var sender: MtbFileSender + private lateinit var requestService: RequestService + private lateinit var applicationEventPublisher: ApplicationEventPublisher + private lateinit var appConfigProperties: AppConfigProperties + private lateinit var consentProcessor: ConsentProcessor + private lateinit var requestProcessor: RequestProcessor - doAnswer { false } - .whenever(requestService) - .isLastRequestWithKnownStatusDeletion(anyValueClass()) + @BeforeEach + fun setup( + @Mock pseudonymizeService: PseudonymizeService, + @Mock transformationService: TransformationService, + @Mock sender: RestMtbFileSender, + @Mock requestService: RequestService, + @Mock applicationEventPublisher: ApplicationEventPublisher, + @Mock consentProcessor: ConsentProcessor, + ) { + this.pseudonymizeService = pseudonymizeService + this.transformationService = transformationService + this.sender = sender + this.requestService = requestService + this.applicationEventPublisher = applicationEventPublisher + this.appConfigProperties = AppConfigProperties() + this.consentProcessor = consentProcessor + + val objectMapper = ObjectMapper() + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + objectMapper, + applicationEventPublisher, + appConfigProperties, + consentProcessor, + ) + } - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) + @Test + fun testShouldSendMtbFileDuplicationAndSaveUnknownRequestStatusAtFirst() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("6vkiti5bk6ikwifpajpt7cygmd3dvw54d6lwfhzlynb3pqtzferq"), + RequestType.MTB_FILE, + SubmissionType.TEST, + RequestStatus.SUCCESS, + Tan.empty(), + Instant.parse("2023-08-08T02:00:00Z"), + ) + } + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + this.requestProcessor.processMtbFile(mtbFile) - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + } - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))) - .build() - ) - .build() - ) + @Test + fun testShouldDetectMtbFileDuplicationAndSendDuplicationEvent() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("4gcjwtjjtcczybsljxepdfpkaeusvd7g3vogfqpmphyffyzfx7dq"), + RequestType.MTB_FILE, + SubmissionType.TEST, + RequestStatus.SUCCESS, + Tan.empty(), + Instant.parse("2023-08-08T02:00:00Z"), ) - .build() - - this.requestProcessor.processMtbFile(mtbFile) - - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) - } - - @Test - fun testShouldDetectMtbFileDuplicationAndSendDuplicationEvent() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("4gcjwtjjtcczybsljxepdfpkaeusvd7g3vogfqpmphyffyzfx7dq"), - RequestType.MTB_FILE, - SubmissionType.TEST, - RequestStatus.SUCCESS, - Tan.empty(), - Instant.parse("2023-08-08T02:00:00Z"), - ) } - .whenever(requestService) - .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() - doAnswer { false } - .whenever(requestService) - .isLastRequestWithKnownStatusDeletion(anyValueClass()) + this.requestProcessor.processMtbFile(mtbFile) - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.DUPLICATION) + } - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + @Test + fun testShouldSendMtbFileAndSendSuccessEvent() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("different"), + RequestType.MTB_FILE, + SubmissionType.TEST, + RequestStatus.SUCCESS, + Tan.empty(), + Instant.parse("2023-08-08T02:00:00Z"), + ) + } + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + this.requestProcessor.processMtbFile(mtbFile) - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } + + @Test + fun testShouldSendMtbFileAndSendErrorEvent() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("different"), + RequestType.MTB_FILE, + SubmissionType.TEST, + RequestStatus.SUCCESS, + Tan.empty(), + Instant.parse("2023-08-08T02:00:00Z"), + ) + } + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.ERROR) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } + .whenever(pseudonymizeService) + .genomDeTan(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata( + MvhMetadata.builder() + .modelProjectConsent( + ModelProjectConsent.builder() + .provisions( + listOf( + Provision.builder() + .type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() + ) + ) .build() ) .build() ) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() + + this.requestProcessor.processMtbFile(mtbFile) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) + } + + @Test + fun testShouldSendMtbFileAdditionIfInitialFileWasAccepted() { + + // One successful and accepted and one blocked initial + val lastRequests = + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.SUCCESS, + Tan.empty(), + Instant.parse("2026-01-05T09:00:00Z"), + submissionAccepted = true, + ), + Request( + 2L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("blocked_initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.BLOCKED_INITIAL, + Tan.empty(), + Instant.parse("2026-01-05T10:00:00Z"), + submissionAccepted = false, + ), ) - .build() - - this.requestProcessor.processMtbFile(mtbFile) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.DUPLICATION) - } - - @Test - fun testShouldSendMtbFileAndSendSuccessEvent() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("different"), - RequestType.MTB_FILE, - SubmissionType.TEST, - RequestStatus.SUCCESS, - Tan.empty(), - Instant.parse("2023-08-08T02:00:00Z"), - ) - } - .whenever(requestService) - .lastMtbFileRequestForPatientPseudonym(anyValueClass()) - doAnswer { false } - .whenever(requestService) - .isLastRequestWithKnownStatusDeletion(anyValueClass()) + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any<DnpmV2MtbFileRequest>()) + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } + .whenever(pseudonymizeService) + .genomDeTan(anyValueClass()) - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) - .build() - ) - .build() - ) + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + ObjectMapper(), + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, ) - .build() - - this.requestProcessor.processMtbFile(mtbFile) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } - - @Test - fun testShouldSendMtbFileAndSendErrorEvent() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("different"), - RequestType.MTB_FILE, - SubmissionType.TEST, - RequestStatus.SUCCESS, - Tan.empty(), - Instant.parse("2023-08-08T02:00:00Z"), - ) - } - .whenever(requestService) - .lastMtbFileRequestForPatientPseudonym(anyValueClass()) - - doAnswer { false } - .whenever(requestService) - .isLastRequestWithKnownStatusDeletion(anyValueClass()) - - doAnswer { MtbFileSender.Response(status = RequestStatus.ERROR) } - .whenever(sender) - .send(any<DnpmV2MtbFileRequest>()) - - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) - - doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } - .whenever(pseudonymizeService) - .genomDeTan(anyValueClass()) - - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .metadata( - MvhMetadata.builder() - .modelProjectConsent( - ModelProjectConsent.builder() - .provisions( - listOf( - Provision.builder() - .type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.SEQUENCING) - .build() - ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() ) .build() ) - .build() - ) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) - .build() - ) - .build() ) - ) - .build() + .build() - this.requestProcessor.processMtbFile(mtbFile) + this.requestProcessor.processMtbFile(mtbFile) - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) - } + val requestCaptor = argumentCaptor<DnpmV2MtbFileRequest>() + verify(sender, times(1)).send(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) + assertThat(requestCaptor.firstValue.content.metadata.transferTan).isEqualTo("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2") - @Test - fun testShouldSendMtbFileAdditionIfInitialFileWasAccepted() { + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } - // One successful and accepted and one blocked initial - val lastRequests = - listOf( - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("initial"), - RequestType.MTB_FILE, - SubmissionType.INITIAL, - RequestStatus.SUCCESS, - Tan.empty(), - Instant.parse("2026-01-05T09:00:00Z"), - submissionAccepted = true, - ), - Request( - 2L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("blocked_initial"), - RequestType.MTB_FILE, - SubmissionType.INITIAL, - RequestStatus.BLOCKED_INITIAL, - Tan.empty(), - Instant.parse("2026-01-05T10:00:00Z"), - submissionAccepted = false, - ), + @Test + fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() { + doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.UNKNOWN) } + .whenever(sender) + .send(any<DeleteRequest>()) + + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, ) - doAnswer { lastRequests } - .whenever(requestService) - .allRequestsByPatientPseudonym(anyValueClass()) + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + } - doAnswer { false } - .whenever(requestService) - .isLastRequestWithKnownStatusDeletion(anyValueClass()) + @Test + fun testShouldSendDeleteRequestAndSendSuccessEvent() { + doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any<DnpmV2MtbFileRequest>()) + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DeleteRequest>()) - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, + ) - doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } - .whenever(pseudonymizeService) - .genomDeTan(anyValueClass()) + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + @Test + fun testShouldSendRequestWithoutConsent() { + doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + doAnswer { it.arguments.first() } + .whenever(transformationService) + .transform(any<Mtb>()) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() + + this.requestProcessor.processMtbFile( + mtbFile, + randomRequestId(), + ) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } + + @Test + fun testShouldSendDeleteRequestAndSendErrorEvent() { + doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + doAnswer { MtbFileSender.Response(status = RequestStatus.ERROR) } + .whenever(sender) + .send(any<DeleteRequest>()) - requestProcessor = - RequestProcessor( - pseudonymizeService, - transformationService, - sender, - requestService, - ObjectMapper(), - applicationEventPublisher, - AppConfigProperties(postInitialSubmissionBlock = true), - consentProcessor, + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, ) - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .metadata(MvhMetadata()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) - .build() - ) - .build() - ) - ) - .build() - - this.requestProcessor.processMtbFile(mtbFile) - - val requestCaptor = argumentCaptor<DnpmV2MtbFileRequest>() - verify(sender, times(1)).send(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) - assertThat(requestCaptor.firstValue.content.metadata.transferTan).isEqualTo("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2") - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } - - @Test - fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() { - doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { MtbFileSender.Response(status = RequestStatus.UNKNOWN) } - .whenever(sender) - .send(any<DeleteRequest>()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, - ) - - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) - } - - @Test - fun testShouldSendDeleteRequestAndSendSuccessEvent() { - doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any<DeleteRequest>()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, - ) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) + } @Test - fun testShouldSendRequestWithoutConsent() { - doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any<DnpmV2MtbFileRequest>()) - - doAnswer { it.arguments.first() } - .whenever(transformationService) - .transform(any<Mtb>()) - - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .metadata(MvhMetadata()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) - .build() - ) - .build() + fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() { + doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, + ) + + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) + } + + @Test + fun testShouldNotDetectMtbFileDuplicationIfDuplicationNotConfigured() { + this.appConfigProperties.duplicationDetection = false + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) ) + .build() + + this.requestProcessor.processMtbFile(mtbFile) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } + + @Test + fun testShouldSaveRequestWithGenomDeTan() { + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } + .whenever(pseudonymizeService) + .genomDeTan(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + ObjectMapper(), + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, ) - .build() - - this.requestProcessor.processMtbFile( - mtbFile, - randomRequestId(), - ) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } - - @Test - fun testShouldSendDeleteRequestAndSendErrorEvent() { - doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { MtbFileSender.Response(status = RequestStatus.ERROR) } - .whenever(sender) - .send(any<DeleteRequest>()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, - ) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) - } - - @Test - fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() { - doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, - ) - - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) - } - - @Test - fun testShouldNotDetectMtbFileDuplicationIfDuplicationNotConfigured() { - this.appConfigProperties.duplicationDetection = false - - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) - - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) - - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any<DnpmV2MtbFileRequest>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) - .build() - ) - .build() + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .metadata(MvhMetadata()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) ) - ) - .build() + .build() - this.requestProcessor.processMtbFile(mtbFile) + this.requestProcessor.processMtbFile(mtbFile) - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.tan).isEqualTo(Tan("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2")) + } - @Test - fun testShouldSaveRequestWithGenomDeTan() { - - doAnswer { false } - .whenever(requestService) - .isLastRequestWithKnownStatusDeletion(anyValueClass()) - - doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } - .whenever(sender) - .send(any<DnpmV2MtbFileRequest>()) - - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) - - doAnswer { "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2" } - .whenever(pseudonymizeService) - .genomDeTan(anyValueClass()) - - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - requestProcessor = - RequestProcessor( - pseudonymizeService, - transformationService, - sender, - requestService, - ObjectMapper(), - applicationEventPublisher, - AppConfigProperties(postInitialSubmissionBlock = true), - consentProcessor, - ) + @Nested + inner class WithInitialSubmissionBlock { + + private lateinit var pseudonymizeService: PseudonymizeService + private lateinit var transformationService: TransformationService + private lateinit var sender: MtbFileSender + private lateinit var requestService: RequestService + private lateinit var applicationEventPublisher: ApplicationEventPublisher + private lateinit var appConfigProperties: AppConfigProperties + private lateinit var consentProcessor: ConsentProcessor + private lateinit var requestProcessor: RequestProcessor + + @BeforeEach + fun setup( + @Mock pseudonymizeService: PseudonymizeService, + @Mock transformationService: TransformationService, + @Mock sender: RestMtbFileSender, + @Mock requestService: RequestService, + @Mock applicationEventPublisher: ApplicationEventPublisher, + @Mock consentProcessor: ConsentProcessor, + ) { + this.pseudonymizeService = pseudonymizeService + this.transformationService = transformationService + this.sender = sender + this.requestService = requestService + this.applicationEventPublisher = applicationEventPublisher + this.appConfigProperties = AppConfigProperties() + this.consentProcessor = consentProcessor + + val objectMapper = ObjectMapper() + + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + objectMapper, + applicationEventPublisher, + appConfigProperties, + consentProcessor, + ) + } + + @Test + fun testShouldNotSendMtbFileIfInitialFileWasSent() { - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .metadata(MvhMetadata()) - .episodesOfCare( + // One failed attempt and one successful but not accepted + val lastRequests = listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) - .build() - ) - .build() + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.ERROR, + Tan.empty(), + Instant.parse("2026-01-05T09:00:00Z"), + submissionAccepted = false, + ), + Request( + 2L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("blocked_initial"), + RequestType.MTB_FILE, + SubmissionType.INITIAL, + RequestStatus.SUCCESS, + Tan.empty(), + Instant.parse("2026-01-05T10:00:00Z"), + submissionAccepted = false, + ), ) - ) - .build() - this.requestProcessor.processMtbFile(mtbFile) + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.tan).isEqualTo(Tan("f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2")) - } + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) - @Nested - inner class WithInitialSubmissionBlock { + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) - private lateinit var pseudonymizeService: PseudonymizeService - private lateinit var transformationService: TransformationService - private lateinit var sender: MtbFileSender - private lateinit var requestService: RequestService - private lateinit var applicationEventPublisher: ApplicationEventPublisher - private lateinit var appConfigProperties: AppConfigProperties - private lateinit var consentProcessor: ConsentProcessor - private lateinit var requestProcessor: RequestProcessor + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - @BeforeEach - fun setup( - @Mock pseudonymizeService: PseudonymizeService, - @Mock transformationService: TransformationService, - @Mock sender: RestMtbFileSender, - @Mock requestService: RequestService, - @Mock applicationEventPublisher: ApplicationEventPublisher, - @Mock consentProcessor: ConsentProcessor, - ) { - this.pseudonymizeService = pseudonymizeService - this.transformationService = transformationService - this.sender = sender - this.requestService = requestService - this.applicationEventPublisher = applicationEventPublisher - this.appConfigProperties = AppConfigProperties() - this.consentProcessor = consentProcessor - - val objectMapper = ObjectMapper() - - requestProcessor = - RequestProcessor( - pseudonymizeService, - transformationService, - sender, - requestService, - objectMapper, - applicationEventPublisher, - appConfigProperties, - consentProcessor, - ) + requestProcessor = + RequestProcessor( + pseudonymizeService, + transformationService, + sender, + requestService, + ObjectMapper(), + applicationEventPublisher, + AppConfigProperties(postInitialSubmissionBlock = true), + consentProcessor, + ) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() + + this.requestProcessor.processMtbFile(mtbFile) + + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.BLOCKED_INITIAL) + + verify(applicationEventPublisher, times(0)).publishEvent(any()) + verify(sender, times(0)).send(any<DnpmV2MtbFileRequest>()) + } } @Test - fun testShouldNotSendMtbFileIfInitialFileWasSent() { - - // One failed attempt and one successful but not accepted - val lastRequests = - listOf( - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("initial"), - RequestType.MTB_FILE, - SubmissionType.INITIAL, - RequestStatus.ERROR, - Tan.empty(), - Instant.parse("2026-01-05T09:00:00Z"), - submissionAccepted = false, - ), - Request( - 2L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("blocked_initial"), - RequestType.MTB_FILE, - SubmissionType.INITIAL, - RequestStatus.SUCCESS, - Tan.empty(), - Instant.parse("2026-01-05T10:00:00Z"), - submissionAccepted = false, - ), - ) - - doAnswer { lastRequests } - .whenever(requestService) - .allRequestsByPatientPseudonym(anyValueClass()) - - doAnswer { it.arguments[0] as String } - .whenever(pseudonymizeService) - .patientPseudonym(anyValueClass()) - - doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - requestProcessor = - RequestProcessor( - pseudonymizeService, - transformationService, - sender, - requestService, - ObjectMapper(), - applicationEventPublisher, - AppConfigProperties(postInitialSubmissionBlock = true), - consentProcessor, - ) - - val mtbFile = - Mtb.builder() - .patient(Patient.builder().id("123").build()) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period( - PeriodDate.builder() - .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) - .build() - ) - .build() - ) - ) - .build() - - this.requestProcessor.processMtbFile(mtbFile) - - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.BLOCKED_INITIAL) - - verify(applicationEventPublisher, times(0)).publishEvent(any()) - verify(sender, times(0)).send(any<DnpmV2MtbFileRequest>()) + fun shouldCatchExceptionsWhenProcessingMtbFileAndSaveError() { + val invalidMtbFile = Mtb.builder().build() + + val success = this.requestProcessor.processMtbFile(invalidMtbFile) + + assertThat(success).isFalse() + + verify(sender, times(0)).send(any<DnpmV2MtbFileRequest>()) + + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isInstanceOf(Request::class.java) + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) } - } - companion object { - val TEST_PATIENT_ID = PatientId("TEST_12345678901") - } + companion object { + val TEST_PATIENT_ID = PatientId("TEST_12345678901") + } } |
