diff --git a/README.md b/README.md index 5d76a96..8575aff 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,16 @@ Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über de Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an das bwHC-Backend gesendet. +Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an nachfolgende URLs gesendet werden: + +| HTTP-Request | URL | Consent-Status im Datensatz | Bemerkung | +|--------------|-----------------------------------------|-----------------------------|---------------------------------------------------------------------------------| +| POST | `https://dnpm.example.com/mtb` | ACTIVE | Die Anwendung verarbeitet den eingehenden Datensatz | +| POST | `https://dnpm.example.com/mtb` | REJECT | Die Anwendung sendet einen Lösch-Request für die im Datensatz angegebene Pat-ID | +| DELETE | `https://dnpm.example.com/mtb/12345678` | - | Die Anwendung sendet einen Lösch-Request für Pat-ID `12345678` | + +Anstelle des Pfads `/mtb` kann auch, wie in Version 0.9 und älter üblich, `/mtbfile` verwendet werden. + ### Datenübermittlung mit Apache Kafka Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung an Apache Kafka übergeben. @@ -42,6 +52,9 @@ In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfi * `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC` * `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_RESPONSE_TOPIC` +Der Pfad zum Versenden von MTB-Daten ist nun offiziell `/mtb`. +In Versionen **nach Version 0.10** wird die Unterstützung des Pfads `/mtbfile` entfernt. + ### Pseudonymisierung der Patienten-ID Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) angegeben ist, wird diese verwendet. @@ -161,7 +174,7 @@ zur Nutzung des MTB-File-Endpunkts eine HTTP-Basic-Authentifizierung voraussetze ![Tokenverwaltung](docs/tokens.png) -In diesem Fall können den Endpunkt für das Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt konfigurieren: +In diesem Fall kann der Endpunkt für das Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt konfiguriert werden: ``` https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile @@ -266,7 +279,7 @@ Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für ### Antworten und Statusauswertung -Anfragen and bwHC-Backend aus Versionen bis 0.9.x wurden wie folgt behandelt: +Anfragen an das bwHC-Backend aus Versionen bis 0.9.x wurden wie folgt behandelt: | HTTP-Response | Status | |----------------|-----------| 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 ded485c..123a84f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -1,7 +1,7 @@ /* * This file is part of ETL-Processor * - * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors + * Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published @@ -28,7 +28,7 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @RestController -@RequestMapping(path = ["mtbfile"]) +@RequestMapping(path = ["mtbfile", "mtb"]) class MtbFileRestController( private val requestProcessor: RequestProcessor, ) { 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 3e5b53a..ade27b4 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -1,7 +1,7 @@ /* * This file is part of ETL-Processor * - * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors + * Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import dev.dnpm.etl.processor.services.RequestProcessor 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 @@ -40,24 +41,122 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders @ExtendWith(MockitoExtension::class) class MtbFileRestControllerTest { - private lateinit var mockMvc: MockMvc - - private lateinit var requestProcessor: RequestProcessor - private val objectMapper = ObjectMapper() - @BeforeEach - fun setup( - @Mock requestProcessor: RequestProcessor - ) { - this.requestProcessor = requestProcessor - val controller = MtbFileRestController(requestProcessor) - this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + @Nested + inner class BwhcRequests { + + private lateinit var mockMvc: MockMvc + + private lateinit var requestProcessor: RequestProcessor + + @BeforeEach + fun setup( + @Mock requestProcessor: RequestProcessor + ) { + this.requestProcessor = requestProcessor + val controller = MtbFileRestController(requestProcessor) + this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } + + @Test + fun shouldProcessPostRequest() { + mockMvc.post("/mtbfile") { + content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE)) + contentType = MediaType.APPLICATION_JSON + }.andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processMtbFile(any()) + } + + @Test + fun shouldProcessPostRequestWithRejectedConsent() { + mockMvc.post("/mtbfile") { + content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED)) + contentType = MediaType.APPLICATION_JSON + }.andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + } + + @Test + fun shouldProcessDeleteRequest() { + mockMvc.delete("/mtbfile/TEST_12345678").andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + } } - @Test - fun shouldProcessMtbFilePostRequest() { - val mtbFile = MtbFile.builder() + @Nested + inner class BwhcRequestsWithAlias { + + private lateinit var mockMvc: MockMvc + + private lateinit var requestProcessor: RequestProcessor + + @BeforeEach + fun setup( + @Mock requestProcessor: RequestProcessor + ) { + this.requestProcessor = requestProcessor + val controller = MtbFileRestController(requestProcessor) + this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } + + @Test + fun shouldProcessPostRequest() { + mockMvc.post("/mtb") { + content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE)) + contentType = MediaType.APPLICATION_JSON + }.andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processMtbFile(any()) + } + + @Test + fun shouldProcessPostRequestWithRejectedConsent() { + mockMvc.post("/mtb") { + content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED)) + contentType = MediaType.APPLICATION_JSON + }.andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + } + + @Test + fun shouldProcessDeleteRequest() { + mockMvc.delete("/mtb/TEST_12345678").andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + } + } + + companion object { + fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder() .withPatient( Patient.builder() .withId("TEST_12345678") @@ -68,7 +167,7 @@ class MtbFileRestControllerTest { .withConsent( Consent.builder() .withId("1") - .withStatus(Consent.Status.ACTIVE) + .withStatus(consentStatus) .withPatient("TEST_12345678") .build() ) @@ -80,66 +179,5 @@ class MtbFileRestControllerTest { .build() ) .build() - - mockMvc.post("/mtbfile") { - content = objectMapper.writeValueAsString(mtbFile) - contentType = MediaType.APPLICATION_JSON - }.andExpect { - status { - isAccepted() - } - } - - verify(requestProcessor, times(1)).processMtbFile(any()) } - - @Test - fun shouldProcessMtbFilePostRequestWithRejectedConsent() { - val mtbFile = MtbFile.builder() - .withPatient( - Patient.builder() - .withId("TEST_12345678") - .withBirthDate("2000-08-08") - .withGender(Patient.Gender.MALE) - .build() - ) - .withConsent( - Consent.builder() - .withId("1") - .withStatus(Consent.Status.REJECTED) - .withPatient("TEST_12345678") - .build() - ) - .withEpisode( - Episode.builder() - .withId("1") - .withPatient("TEST_12345678") - .withPeriod(PeriodStart("2023-08-08")) - .build() - ) - .build() - - mockMvc.post("/mtbfile") { - content = objectMapper.writeValueAsString(mtbFile) - contentType = MediaType.APPLICATION_JSON - }.andExpect { - status { - isAccepted() - } - } - - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) - } - - @Test - fun shouldProcessMtbFileDeleteRequest() { - mockMvc.delete("/mtbfile/TEST_12345678").andExpect { - status { - isAccepted() - } - } - - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) - } - -} \ No newline at end of file +}