From 7be91444a867774362eb5b57bdd246fb50189e7d Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Tue, 6 Jan 2026 16:34:12 +0100 Subject: feat: block further initial submissions (#232) --- .../etl/processor/services/RequestProcessorTest.kt | 232 +++++++++++++++++++++ .../etl/processor/services/RequestServiceTest.kt | 11 + .../processor/services/ResponseProcessorTest.kt | 2 + .../services/TransformationServiceTest.kt | 11 +- 4 files changed, 253 insertions(+), 3 deletions(-) (limited to 'src/test') 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 4bd3fc1..851c1a1 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt @@ -28,6 +28,7 @@ import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType +import dev.dnpm.etl.processor.monitoring.SubmissionType import dev.dnpm.etl.processor.output.DeleteRequest import dev.dnpm.etl.processor.output.DnpmV2MtbFileRequest import dev.dnpm.etl.processor.output.MtbFileSender @@ -39,6 +40,7 @@ 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 import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock @@ -104,6 +106,7 @@ class RequestProcessorTest { PatientId("P1"), Fingerprint("6vkiti5bk6ikwifpajpt7cygmd3dvw54d6lwfhzlynb3pqtzferq"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.SUCCESS, Instant.parse("2023-08-08T02:00:00Z"), ) @@ -159,6 +162,7 @@ class RequestProcessorTest { PatientId("P1"), Fingerprint("4gcjwtjjtcczybsljxepdfpkaeusvd7g3vogfqpmphyffyzfx7dq"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.SUCCESS, Instant.parse("2023-08-08T02:00:00Z"), ) @@ -214,6 +218,7 @@ class RequestProcessorTest { PatientId("P1"), Fingerprint("different"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.SUCCESS, Instant.parse("2023-08-08T02:00:00Z"), ) @@ -273,6 +278,7 @@ class RequestProcessorTest { PatientId("P1"), Fingerprint("different"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.SUCCESS, Instant.parse("2023-08-08T02:00:00Z"), ) @@ -338,6 +344,102 @@ class RequestProcessorTest { 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, + 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, + Instant.parse("2026-01-05T10:00:00Z"), + submissionAccepted = false, + ), + ) + + doAnswer { lastRequests } + .whenever(requestService) + .allRequestsByPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any()) + + 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()) + .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() + verify(sender, times(1)).send(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.content.metadata.type).isEqualTo(MvhSubmissionType.ADDITION) + + val eventCaptor = argumentCaptor() + 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()) @@ -452,6 +554,136 @@ class RequestProcessorTest { assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) } + @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() { + + // 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, + 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, + 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()) + + 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() + 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()) + } + } + companion object { val TEST_PATIENT_ID = PatientId("TEST_12345678901") } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt index bc0286c..fdb7578 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt @@ -24,6 +24,7 @@ import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestRepository import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType +import dev.dnpm.etl.processor.monitoring.SubmissionType import java.time.Instant import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -51,6 +52,7 @@ class RequestServiceTest { PatientId("PX"), Fingerprint("dummy"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.SUCCESS, Instant.parse("2023-08-08T02:00:00Z"), ) @@ -72,6 +74,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.WARNING, Instant.parse("2023-07-07T00:00:00Z"), ), @@ -82,6 +85,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdefd"), RequestType.DELETE, + SubmissionType.TEST, RequestStatus.WARNING, Instant.parse("2023-07-07T02:00:00Z"), ), @@ -92,6 +96,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.UNKNOWN, Instant.parse("2023-08-11T00:00:00Z"), ), @@ -113,6 +118,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.WARNING, Instant.parse("2023-07-07T00:00:00Z"), ), @@ -123,6 +129,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.WARNING, Instant.parse("2023-07-07T02:00:00Z"), ), @@ -133,6 +140,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.UNKNOWN, Instant.parse("2023-08-11T00:00:00Z"), ), @@ -154,6 +162,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.DELETE, + SubmissionType.TEST, RequestStatus.SUCCESS, Instant.parse("2023-07-07T02:00:00Z"), ), @@ -164,6 +173,7 @@ class RequestServiceTest { PatientId("P2"), Fingerprint("0123456789abcdef2"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.WARNING, Instant.parse("2023-08-08T00:00:00Z"), ), @@ -200,6 +210,7 @@ class RequestServiceTest { PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.DELETE, + SubmissionType.TEST, RequestStatus.SUCCESS, Instant.parse("2023-07-07T02:00:00Z"), ) diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt index 16a5791..804b91c 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt @@ -23,6 +23,7 @@ import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType +import dev.dnpm.etl.processor.monitoring.SubmissionType import java.time.Instant import java.util.* import org.assertj.core.api.Assertions.assertThat @@ -52,6 +53,7 @@ class ResponseProcessorTest { PatientId("1"), Fingerprint("dummyfingerprint"), RequestType.MTB_FILE, + SubmissionType.TEST, RequestStatus.UNKNOWN, ) diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt index a4af214..3af5097 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt @@ -24,11 +24,11 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode import dev.dnpm.etl.processor.config.JacksonConfig 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.Test -import java.time.Instant -import java.util.* class TransformationServiceTest { @@ -149,7 +149,12 @@ class TransformationServiceTest { ) .build() val consent = ConsentProcessorTest.getDummyGenomDeConsent() - val jsonNode = ObjectMapper().readValue(FhirContext.forR4().newJsonParser().encodeToString(consent), ObjectNode::class.java) + val jsonNode = + ObjectMapper() + .readValue( + FhirContext.forR4().newJsonParser().encodeToString(consent), + ObjectNode::class.java, + ) mvhMetadata.researchConsents = mutableListOf() mvhMetadata.researchConsents.add(MvhMetadata.ResearchConsent.from(jsonNode)) -- cgit v1.2.3