mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-07-01 14:12:55 +00:00
Compare commits
1 Commits
63-check-c
...
v0.10.0
Author | SHA1 | Date | |
---|---|---|---|
ec096d9c81 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -39,5 +39,3 @@ out/
|
|||||||
.vscode/
|
.vscode/
|
||||||
/dev/gpas*
|
/dev/gpas*
|
||||||
/deploy/.env
|
/deploy/.env
|
||||||
/dev/gICS*
|
|
||||||
/dev/gPAS*
|
|
||||||
|
45
README.md
45
README.md
@ -52,6 +52,8 @@ Soll noch das alte bwHC-Backend verwendet werden, so ist die Umgebungsvariable
|
|||||||
|
|
||||||
In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen entfernt:
|
In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen entfernt:
|
||||||
|
|
||||||
|
* `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`: Nutzen Sie hier, wie unter [_Integration eines eigenen Root CA
|
||||||
|
Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben, das Einbinden eigener Zertifikate.
|
||||||
* `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC`
|
* `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`
|
* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`
|
||||||
|
|
||||||
@ -66,9 +68,11 @@ Ist diese nicht gesetzt. wird intern eine Anonymisierung der Patienten-ID vorgen
|
|||||||
* `APP_PSEUDONYMIZE_PREFIX`: Standortbezogenes Präfix - `UNKNOWN`, wenn nicht gesetzt
|
* `APP_PSEUDONYMIZE_PREFIX`: Standortbezogenes Präfix - `UNKNOWN`, wenn nicht gesetzt
|
||||||
* `APP_PSEUDONYMIZE_GENERATOR`: `BUILDIN` oder `GPAS` - `BUILDIN`, wenn nicht gesetzt
|
* `APP_PSEUDONYMIZE_GENERATOR`: `BUILDIN` oder `GPAS` - `BUILDIN`, wenn nicht gesetzt
|
||||||
|
|
||||||
**Hinweis**
|
**Hinweise**:
|
||||||
|
|
||||||
Die Pseudonymisierung erfolgt im ETL-Prozessor nur für die Patienten-ID.
|
* Der alte Konfigurationsparameter `APP_PSEUDONYMIZER` mit den Werten `GPAS` oder `BUILDIN` sollte nicht mehr verwendet
|
||||||
|
werden.
|
||||||
|
* Die Pseudonymisierung erfolgt im ETL-Prozessor nur für die Patienten-ID.
|
||||||
Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den aktuellen Kontext nicht
|
Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den aktuellen Kontext nicht
|
||||||
vergleichbare IDs bereitzustellen.
|
vergleichbare IDs bereitzustellen.
|
||||||
|
|
||||||
@ -86,36 +90,13 @@ Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfiguri
|
|||||||
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname
|
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname
|
||||||
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
|
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
|
||||||
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
|
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
|
||||||
|
* ~~`APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`~~: **Veraltet** - Root Zertifikat für gPas, falls es dediziert hinzugefügt werden muss.
|
||||||
|
**Wird in nach Version 0.10 entfernt**
|
||||||
|
|
||||||
### Einwilligung gICS
|
Der Konfigurationsparameter `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION` sollte nicht mehr verwendet werden und wird nach
|
||||||
Ab gIcs Version 2.13.0 kann per [REST-Schnittstelle](https://simplifier.net/guide/ttp-fhir-gateway-ig/ImplementationGuide-markdown-Einwilligungsmanagement-Operations-isConsented?version=current) der Einwilligungsstatus abgefragt werden.
|
Version 0.10 entfernt.
|
||||||
Vor der MTB-Übertragung kann der zum Sendezeitpunkt verfügbarer Einwilligungsstatus über Endpunkt *isConsented* abgefragt werden.
|
Stattdessen sollte das Root Zertifikat wie unter [_Integration eines eigenen Root CA
|
||||||
|
Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben eingebunden werden.
|
||||||
Falls Anbindung an gICS aktiviert wurde, wird der Einwilligungsstatus der MTB Datei ignoriert.
|
|
||||||
Stattdessen werden vorhandene Einwilligungen abgefragt und in die MTB Datei eingebettet.
|
|
||||||
|
|
||||||
Es werden zwei Einwilligungsdomänen unterstützt, eine für Broad Consent und als zweites GenomDE Modelvorhaben §64e.
|
|
||||||
|
|
||||||
#### Hinweise
|
|
||||||
1. Die aktuelle Impl. nimmt an, dass die hinterlegten Domänen der Einwilligungen ausschließlich für die genannten Art von Einwilligungen genutzt werden. Es finde keine weitere Filterung statt. Wir fragen pro Domäne die Schnittstelle `CurrentPolicyStatesForPerson` - siehe auch [IG TTP-FHIR Gateway
|
|
||||||
](https://www.ths-greifswald.de/wp-content/uploads/tools/fhirgw/ig/2024-3-0/ImplementationGuide-markdown-Einwilligungsmanagement-Operations-currentPolicyStatesForPerson.html) ab.
|
|
||||||
2. Die Einwilligung wird für den Patienten-Identifier der MTB abgerufen und anschließend durch das DNPM Pseudonym ersetzt.
|
|
||||||
3. Abfragen von Einwilligungen über gesonderte Pseudonyme anstatt des MTB-Identifiers fehlt in der ersten Implementierung.
|
|
||||||
4. Bei Verarbeitung von MTB Version 1.x Inhalten ist eine positive Einwilligung für die
|
|
||||||
Weiterverarbeitung notwendig. Das Fehlen einer Einwilligung löst die Löschung des Patienten im Brückenkopf aus.
|
|
||||||
|
|
||||||
#### Konfiguration
|
|
||||||
* `APP_CONSENT_GICS_ENABLED`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn nicht gesetzt.
|
|
||||||
* `APP_CONSENT_GICS_CHECKGNOMEDE`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn nicht gesetzt. Versuche Einwilligungsdaten zu GENOM DE Modelvorhaben über gIcs abzurufen.
|
|
||||||
* `APP_CONSENT_GICS_CHECKBROADCONSENT`: Aktiviert oder deaktiviert `true` oder `false`, `false` wenn nicht gesetzt. Versuche Einwilligungsdaten zu Broad Consent über gIcs abzurufen.
|
|
||||||
* `APP_CONSENT_GICS_URI`: URI der gICS-Instanz (z.B. `http://localhost:8090/ttp-fhir/fhir/gics`)
|
|
||||||
* `APP_CONSENT_GICS_USERNAME`: gIcs Basic-Auth Benutzername
|
|
||||||
* `APP_CONSENT_GICS_PASSWORD`: gIcs Basic-Auth Passwort
|
|
||||||
* `APP_CONSENT_GICS_PERSONIDENTIFIERSYSTEM`: Derzeit wird nur die PID unterstützt. wenn leer wird `https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID` angenommen
|
|
||||||
* `APP_CONSENT_GICS_BROADCONSENTDOMAINNAME`: Domäne in der gIcs Broad Consent Einwilligungen verwaltet. Falls Wert leer, wird `MII` angenommen.
|
|
||||||
* `APP_CONSENT_GICS_GNOMDECONSENTDOMAINNAME`: Domäne in der gIcs GenomDE Modelvorhaben §64e Einwilligungen verwaltet. Falls Wert leer, wird `GenomDE_MV` angenommen.
|
|
||||||
* `APP_CONSENT_GICS_POLICYCODE`: Die entscheidende Objekt-ID der zu prüfenden Einwilligung-Regel. Falls leer wird `2.16.840.1.113883.3.1937.777.24.5.3.6` angenommen.
|
|
||||||
* `APP_CONSENT_GICS_POLICYSYSTEM`: Das System der Einwilligung-Regel der Objekt-IDs. Falls leer wird `urn:oid:2.16.840.1.113883.3.1937.777.24.5.3` angenommen.
|
|
||||||
|
|
||||||
### Anmeldung mit einem Passwort
|
### Anmeldung mit einem Passwort
|
||||||
|
|
||||||
@ -245,7 +226,9 @@ Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an DNP
|
|||||||
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an ein Kafka-Topic übermittelt wird:
|
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an ein Kafka-Topic übermittelt wird:
|
||||||
|
|
||||||
* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen.
|
* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen.
|
||||||
|
Ersetzt ~~`APP_KAFKA_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
|
||||||
* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_response".
|
* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_response".
|
||||||
|
Ersetzt ~~`APP_KAFKA_RESPONSE_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
|
||||||
* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_group".
|
* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_group".
|
||||||
* `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste
|
* `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
war
|
war
|
||||||
id("org.springframework.boot") version "3.5.0"
|
id("org.springframework.boot") version "3.3.10"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
kotlin("jvm") version "1.9.25"
|
kotlin("jvm") version "1.9.25"
|
||||||
kotlin("plugin.spring") version "1.9.25"
|
kotlin("plugin.spring") version "1.9.25"
|
||||||
@ -13,12 +13,12 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.dnpm"
|
group = "dev.dnpm"
|
||||||
version = "0.11.0-SNAPSHOT"
|
version = "0.10.0"
|
||||||
|
|
||||||
var versions = mapOf(
|
var versions = mapOf(
|
||||||
"bwhc-dto-java" to "0.4.0",
|
"bwhc-dto-java" to "0.4.0",
|
||||||
"mtb-dto" to "0.1.0-SNAPSHOT",
|
|
||||||
"hapi-fhir" to "7.6.0",
|
"hapi-fhir" to "7.6.0",
|
||||||
|
"commons-compress" to "1.26.2",
|
||||||
"mockito-kotlin" to "5.4.0",
|
"mockito-kotlin" to "5.4.0",
|
||||||
"archunit" to "1.3.0",
|
"archunit" to "1.3.0",
|
||||||
// Webjars
|
// Webjars
|
||||||
@ -49,18 +49,9 @@ configurations {
|
|||||||
compileOnly {
|
compileOnly {
|
||||||
extendsFrom(configurations.annotationProcessor.get())
|
extendsFrom(configurations.annotationProcessor.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
all {
|
|
||||||
resolutionStrategy {
|
|
||||||
cacheChangingModulesFor(5, "minutes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
|
||||||
url = uri("https://git.dnpm.dev/api/packages/public-snapshots/maven")
|
|
||||||
}
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://git.dnpm.dev/api/packages/public/maven")
|
url = uri("https://git.dnpm.dev/api/packages/public/maven")
|
||||||
}
|
}
|
||||||
@ -82,7 +73,6 @@ dependencies {
|
|||||||
implementation("commons-codec:commons-codec")
|
implementation("commons-codec:commons-codec")
|
||||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||||
implementation("de.ukw.ccc:bwhc-dto-java:${versions["bwhc-dto-java"]}")
|
implementation("de.ukw.ccc:bwhc-dto-java:${versions["bwhc-dto-java"]}")
|
||||||
implementation("dev.pcvolkmer.mv64e:mtb-dto:${versions["mtb-dto"]}") { isChanging = true }
|
|
||||||
implementation("ca.uhn.hapi.fhir:hapi-fhir-base:${versions["hapi-fhir"]}")
|
implementation("ca.uhn.hapi.fhir:hapi-fhir-base:${versions["hapi-fhir"]}")
|
||||||
implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:${versions["hapi-fhir"]}")
|
implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:${versions["hapi-fhir"]}")
|
||||||
implementation("org.apache.httpcomponents.client5:httpclient5")
|
implementation("org.apache.httpcomponents.client5:httpclient5")
|
||||||
@ -109,8 +99,10 @@ dependencies {
|
|||||||
integrationTestImplementation("org.testcontainers:junit-jupiter")
|
integrationTestImplementation("org.testcontainers:junit-jupiter")
|
||||||
integrationTestImplementation("org.testcontainers:postgresql")
|
integrationTestImplementation("org.testcontainers:postgresql")
|
||||||
integrationTestImplementation("com.tngtech.archunit:archunit:${versions["archunit"]}")
|
integrationTestImplementation("com.tngtech.archunit:archunit:${versions["archunit"]}")
|
||||||
integrationTestImplementation("org.htmlunit:htmlunit")
|
integrationTestImplementation("net.sourceforge.htmlunit:htmlunit")
|
||||||
integrationTestImplementation("org.springframework:spring-webflux")
|
integrationTestImplementation("org.springframework:spring-webflux")
|
||||||
|
// Override dependency version from org.testcontainers:junit-jupiter - CVE-2024-26308, CVE-2024-25710
|
||||||
|
integrationTestImplementation("org.apache.commons:commons-compress:${versions["commons-compress"]}")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
@ -127,9 +119,8 @@ tasks.withType<Test> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Test>("integrationTest") {
|
task<Test>("integrationTest") {
|
||||||
description = "Runs integration tests"
|
description = "Runs integration tests"
|
||||||
group = "verification"
|
|
||||||
|
|
||||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||||
|
@ -16,11 +16,6 @@ services:
|
|||||||
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true
|
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true
|
||||||
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
|
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
|
||||||
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
||||||
healthcheck:
|
|
||||||
test: kafka-topics --bootstrap-server kafka:9092 --list
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
## Use AKHQ as Kafka web frontend
|
## Use AKHQ as Kafka web frontend
|
||||||
akhq:
|
akhq:
|
||||||
|
@ -2,55 +2,31 @@ version: '3.7'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
zoo:
|
zoo1:
|
||||||
image: zookeeper:3.9.2
|
image: zookeeper:3.8.0
|
||||||
restart: unless-stopped
|
hostname: zoo1
|
||||||
ports:
|
ports:
|
||||||
- "2181:2181"
|
- "2181:2181"
|
||||||
environment:
|
environment:
|
||||||
ZOO_MY_ID: 1
|
ZOO_MY_ID: 1
|
||||||
ZOO_PORT: 2181
|
ZOO_PORT: 2181
|
||||||
ZOO_SERVERS: server.1=zoo:2888:3888;2181
|
ZOO_SERVERS: server.1=zoo1:2888:3888;2181
|
||||||
|
|
||||||
kafka:
|
kafka1:
|
||||||
image: confluentinc/cp-kafka:7.6.1
|
image: confluentinc/cp-kafka:7.2.1
|
||||||
|
hostname: kafka1
|
||||||
ports:
|
ports:
|
||||||
- "9092:9092"
|
- "9092:9092"
|
||||||
environment:
|
environment:
|
||||||
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka:19092,LISTENER_DOCKER_EXTERNAL://172.17.0.1:9093,LISTENER_EXTERNAL://127.0.0.1:9092
|
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka1:19092,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092
|
||||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT,LISTENER_EXTERNAL:PLAINTEXT
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT
|
||||||
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
|
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
|
||||||
KAFKA_ZOOKEEPER_CONNECT: zoo:2181
|
KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
|
||||||
KAFKA_BROKER_ID: 1
|
KAFKA_BROKER_ID: 1
|
||||||
KAFKA_LOG4J_LOGGERS: kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO
|
KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
|
||||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||||
KAFKA_MESSAGE_MAX_BYTES: 5242880
|
|
||||||
KAFKA_REPLICA_FETCH_MAX_BYTES: 5242880
|
|
||||||
KAFKA_COMPRESSION_TYPE: gzip
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- zoo
|
- zoo1
|
||||||
healthcheck:
|
|
||||||
test: kafka-topics --bootstrap-server kafka:9092 --list
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
akhq:
|
|
||||||
image: tchiotludo/akhq:0.25.0
|
|
||||||
environment:
|
|
||||||
AKHQ_CONFIGURATION: |
|
|
||||||
akhq:
|
|
||||||
ui-options:
|
|
||||||
topic.show-all-consumer-groups: true
|
|
||||||
topic-data.sort: NEWEST
|
|
||||||
connections:
|
|
||||||
docker-kafka-server:
|
|
||||||
properties:
|
|
||||||
bootstrap.servers: "kafka:19092"
|
|
||||||
ports:
|
|
||||||
- "9000:8080"
|
|
||||||
depends_on:
|
|
||||||
- kafka
|
|
||||||
|
|
||||||
kafka-rest-proxy:
|
kafka-rest-proxy:
|
||||||
image: confluentinc/cp-kafka-rest:7.2.1
|
image: confluentinc/cp-kafka-rest:7.2.1
|
||||||
@ -64,8 +40,8 @@ services:
|
|||||||
KAFKA_REST_HOST_NAME: kafka-rest-proxy
|
KAFKA_REST_HOST_NAME: kafka-rest-proxy
|
||||||
KAFKA_REST_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:19092
|
KAFKA_REST_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:19092
|
||||||
depends_on:
|
depends_on:
|
||||||
- zoo
|
- zoo1
|
||||||
- kafka
|
- kafka1
|
||||||
|
|
||||||
kafka-connect:
|
kafka-connect:
|
||||||
image: confluentinc/cp-kafka-connect:7.2.1
|
image: confluentinc/cp-kafka-connect:7.2.1
|
||||||
@ -91,6 +67,24 @@ services:
|
|||||||
#volumes:
|
#volumes:
|
||||||
# - ./connectors:/etc/kafka-connect/jars/
|
# - ./connectors:/etc/kafka-connect/jars/
|
||||||
depends_on:
|
depends_on:
|
||||||
- zoo
|
- zoo1
|
||||||
- kafka
|
- kafka1
|
||||||
- kafka-rest-proxy
|
- kafka-rest-proxy
|
||||||
|
|
||||||
|
akhq:
|
||||||
|
image: tchiotludo/akhq:0.21.0
|
||||||
|
environment:
|
||||||
|
AKHQ_CONFIGURATION: |
|
||||||
|
akhq:
|
||||||
|
connections:
|
||||||
|
docker-kafka-server:
|
||||||
|
properties:
|
||||||
|
bootstrap.servers: "kafka1:19092"
|
||||||
|
connect:
|
||||||
|
- name: "kafka-connect"
|
||||||
|
url: "http://kafka-connect:8083"
|
||||||
|
ports:
|
||||||
|
- "8084:8080"
|
||||||
|
depends_on:
|
||||||
|
- kafka1
|
||||||
|
- kafka-connect
|
||||||
|
@ -23,7 +23,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.output.BwhcV1MtbFileRequest
|
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
@ -34,10 +33,10 @@ import org.mockito.kotlin.*
|
|||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.post
|
import org.springframework.test.web.servlet.post
|
||||||
@ -46,7 +45,7 @@ import org.testcontainers.junit.jupiter.Testcontainers
|
|||||||
@Testcontainers
|
@Testcontainers
|
||||||
@ExtendWith(SpringExtension::class)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@MockitoBean(types = [MtbFileSender::class])
|
@MockBean(MtbFileSender::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.rest.uri=http://example.com",
|
"app.rest.uri=http://example.com",
|
||||||
@ -74,7 +73,7 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
)
|
)
|
||||||
inner class TransformationTest {
|
inner class TransformationTest {
|
||||||
|
|
||||||
@MockitoBean
|
@MockBean
|
||||||
private lateinit var mtbFileSender: MtbFileSender
|
private lateinit var mtbFileSender: MtbFileSender
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -92,7 +91,7 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
fun mtbFileIsTransformed() {
|
fun mtbFileIsTransformed() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(RequestStatus.SUCCESS)
|
MtbFileSender.Response(RequestStatus.SUCCESS)
|
||||||
}.whenever(mtbFileSender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(mtbFileSender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -135,9 +134,9 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val captor = argumentCaptor<BwhcV1MtbFileRequest>()
|
val captor = argumentCaptor<MtbFileSender.MtbFileRequest>()
|
||||||
verify(mtbFileSender).send(captor.capture())
|
verify(mtbFileSender).send(captor.capture())
|
||||||
assertThat(captor.firstValue.content.diagnoses).hasSize(1).allMatch { diagnosis ->
|
assertThat(captor.firstValue.mtbFile.diagnoses).hasSize(1).allMatch { diagnosis ->
|
||||||
diagnosis.icd10.version == "2014"
|
diagnosis.icd10.version == "2014"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,9 @@ import dev.dnpm.etl.processor.output.KafkaMtbFileSender
|
|||||||
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
||||||
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
||||||
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.dnpm.etl.processor.security.TokenRepository
|
import dev.dnpm.etl.processor.security.TokenRepository
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -36,25 +36,24 @@ import org.junit.jupiter.api.assertThrows
|
|||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException
|
||||||
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
|
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBeans
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@ContextConfiguration(
|
@ContextConfiguration(classes = [
|
||||||
classes = [
|
|
||||||
AppConfiguration::class,
|
AppConfiguration::class,
|
||||||
AppSecurityConfiguration::class,
|
AppSecurityConfiguration::class,
|
||||||
KafkaAutoConfiguration::class,
|
KafkaAutoConfiguration::class,
|
||||||
AppKafkaConfiguration::class,
|
AppKafkaConfiguration::class,
|
||||||
AppRestConfiguration::class
|
AppRestConfiguration::class
|
||||||
]
|
])
|
||||||
)
|
@MockBean(ObjectMapper::class)
|
||||||
@MockitoBean(types = [ObjectMapper::class])
|
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
@ -87,7 +86,7 @@ class AppConfigurationTest {
|
|||||||
"app.kafka.group-id=test"
|
"app.kafka.group-id=test"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(types = [RequestRepository::class])
|
@MockBean(RequestRepository::class)
|
||||||
inner class AppConfigurationKafkaTest(private val context: ApplicationContext) {
|
inner class AppConfigurationKafkaTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -146,7 +145,7 @@ class AppConfigurationTest {
|
|||||||
"app.kafka.group-id=test"
|
"app.kafka.group-id=test"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(types = [RequestProcessor::class])
|
@MockBean(RequestProcessor::class)
|
||||||
inner class AppConfigurationUsingKafkaInputTest(private val context: ApplicationContext) {
|
inner class AppConfigurationUsingKafkaInputTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -182,7 +181,40 @@ class AppConfigurationTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin"
|
"app.pseudonymize.generator=",
|
||||||
|
"app.pseudonymizer=buildin",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
inner class AppConfigurationPseudonymizerBuildinTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldUseConfiguredGenerator() {
|
||||||
|
assertThat(context.getBean(AnonymizingGenerator::class.java)).isNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestPropertySource(
|
||||||
|
properties = [
|
||||||
|
"app.pseudonymize.generator=",
|
||||||
|
"app.pseudonymizer=gpas",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
inner class AppConfigurationPseudonymizerGpasTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldUseConfiguredGenerator() {
|
||||||
|
assertThat(context.getBean(GpasPseudonymGenerator::class.java)).isNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestPropertySource(
|
||||||
|
properties = [
|
||||||
|
"app.pseudonymize.generator=buildin",
|
||||||
|
"app.pseudonymizer=",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class AppConfigurationPseudonymizeGeneratorBuildinTest(private val context: ApplicationContext) {
|
inner class AppConfigurationPseudonymizeGeneratorBuildinTest(private val context: ApplicationContext) {
|
||||||
@ -197,7 +229,8 @@ class AppConfigurationTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=gpas"
|
"app.pseudonymize.generator=gpas",
|
||||||
|
"app.pseudonymizer=",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) {
|
inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) {
|
||||||
@ -215,13 +248,11 @@ class AppConfigurationTest {
|
|||||||
"app.security.enable-tokens=true"
|
"app.security.enable-tokens=true"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBeans(value = [
|
||||||
types = [
|
MockBean(InMemoryUserDetailsManager::class),
|
||||||
InMemoryUserDetailsManager::class,
|
MockBean(PasswordEncoder::class),
|
||||||
PasswordEncoder::class,
|
MockBean(TokenRepository::class)
|
||||||
TokenRepository::class
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class AppConfigurationTokenEnabledTest(private val context: ApplicationContext) {
|
inner class AppConfigurationTokenEnabledTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -232,13 +263,11 @@ class AppConfigurationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@MockitoBean(
|
@MockBeans(value = [
|
||||||
types = [
|
MockBean(InMemoryUserDetailsManager::class),
|
||||||
InMemoryUserDetailsManager::class,
|
MockBean(PasswordEncoder::class),
|
||||||
PasswordEncoder::class,
|
MockBean(TokenRepository::class)
|
||||||
TokenRepository::class
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class AppConfigurationTokenDisabledTest(private val context: ApplicationContext) {
|
inner class AppConfigurationTokenDisabledTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -23,9 +23,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.anyValueClass
|
import dev.dnpm.etl.processor.anyValueClass
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.consent.ConsentCheckFileBased
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.consent.ICheckConsent
|
|
||||||
import dev.dnpm.etl.processor.security.TokenRepository
|
import dev.dnpm.etl.processor.security.TokenRepository
|
||||||
import dev.dnpm.etl.processor.security.UserRoleRepository
|
import dev.dnpm.etl.processor.security.UserRoleRepository
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
@ -34,16 +31,19 @@ import org.junit.jupiter.api.Nested
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.never
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.delete
|
import org.springframework.test.web.servlet.delete
|
||||||
@ -54,18 +54,16 @@ import org.springframework.test.web.servlet.post
|
|||||||
@ContextConfiguration(
|
@ContextConfiguration(
|
||||||
classes = [
|
classes = [
|
||||||
MtbFileRestController::class,
|
MtbFileRestController::class,
|
||||||
AppSecurityConfiguration::class,
|
AppSecurityConfiguration::class
|
||||||
ConsentCheckFileBased::class, ICheckConsent::class
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(types = [TokenRepository::class, RequestProcessor::class])
|
@MockBean(TokenRepository::class, RequestProcessor::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
"app.security.admin-user=admin",
|
"app.security.admin-user=admin",
|
||||||
"app.security.admin-password={noop}very-secret",
|
"app.security.admin-password={noop}very-secret",
|
||||||
"app.security.enable-tokens=true",
|
"app.security.enable-tokens=true"
|
||||||
"app.consent.gics.enabled=false"
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
class MtbFileRestControllerTest {
|
class MtbFileRestControllerTest {
|
||||||
@ -93,7 +91,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -106,7 +104,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -119,7 +117,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, never()).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -132,7 +130,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isForbidden() }
|
status { isForbidden() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, never()).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -143,7 +141,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -154,19 +152,18 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processDeletion(anyValueClass(), any())
|
verify(requestProcessor, never()).processDeletion(anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::class])
|
@MockBean(UserRoleRepository::class, ClientRegistrationRepository::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
"app.security.admin-user=admin",
|
"app.security.admin-user=admin",
|
||||||
"app.security.admin-password={noop}very-secret",
|
"app.security.admin-password={noop}very-secret",
|
||||||
"app.security.enable-tokens=true",
|
"app.security.enable-tokens=true",
|
||||||
"app.security.enable-oidc=true",
|
"app.security.enable-oidc=true"
|
||||||
"app.consent.gics.enabled=false"
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class WithOidcEnabled {
|
inner class WithOidcEnabled {
|
||||||
@ -180,7 +177,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -193,7 +190,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ import org.junit.jupiter.api.extension.ExtendWith
|
|||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
|
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
|
||||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
@ -39,7 +39,7 @@ import java.time.Instant
|
|||||||
@DataJdbcTest
|
@DataJdbcTest
|
||||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||||
@Transactional
|
@Transactional
|
||||||
@MockitoBean(types = [MtbFileSender::class])
|
@MockBean(MtbFileSender::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin",
|
"app.pseudonymize.generator=buildin",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.pseudonym
|
package dev.dnpm.etl.processor.pseudonym
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig
|
|
||||||
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
@ -43,37 +42,30 @@ class GpasPseudonymGeneratorTest {
|
|||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
private lateinit var generator: GpasPseudonymGenerator
|
private lateinit var generator: GpasPseudonymGenerator
|
||||||
private lateinit var restTemplate: RestTemplate
|
private lateinit var restTemplate: RestTemplate
|
||||||
private var appFhirConfig: AppFhirConfig = AppFhirConfig()
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
val gPasConfigProperties = GPasConfigProperties(
|
val gPasConfigProperties = GPasConfigProperties(
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
"http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
||||||
"test",
|
"test",
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
this.restTemplate = RestTemplate()
|
this.restTemplate = RestTemplate()
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
this.generator =
|
this.generator = GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate)
|
||||||
GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate, appFhirConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldReturnExpectedPseudonym() {
|
fun shouldReturnExpectedPseudonym() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.OK).body(
|
withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890"))
|
||||||
getDummyResponseBody(
|
|
||||||
"1234",
|
|
||||||
"test",
|
|
||||||
"test1234ABCDEF567890"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +76,7 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun shouldThrowExceptionIfGpasNotAvailable() {
|
fun shouldThrowExceptionIfGpasNotAvailable() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withException(IOException("Simulated IO error")).createResponse(it)
|
withException(IOException("Simulated IO error")).createResponse(it)
|
||||||
}
|
}
|
||||||
@ -96,13 +88,10 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
|
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.FOUND)
|
withStatus(HttpStatus.FOUND)
|
||||||
.header(
|
.header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
HttpHeaders.LOCATION,
|
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate"
|
|
||||||
)
|
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
@ -42,7 +42,7 @@ import java.time.Instant
|
|||||||
@ExtendWith(SpringExtension::class)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@Transactional
|
@Transactional
|
||||||
@MockitoBean(types = [MtbFileSender::class])
|
@MockBean(MtbFileSender::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin",
|
"app.pseudonymize.generator=buildin",
|
||||||
|
@ -19,22 +19,21 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
import dev.dnpm.etl.processor.monitoring.GIcsConnectionCheckService
|
|
||||||
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
import dev.dnpm.etl.processor.security.Role
|
import dev.dnpm.etl.processor.security.Role
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
|
||||||
import dev.dnpm.etl.processor.security.UserRoleService
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
|
import dev.dnpm.etl.processor.security.UserRoleService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.htmlunit.html.HtmlPage
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -47,6 +46,7 @@ import org.mockito.kotlin.verify
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
||||||
@ -55,7 +55,6 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ
|
|||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.test.web.servlet.*
|
import org.springframework.test.web.servlet.*
|
||||||
@ -82,17 +81,14 @@ abstract class MockSink : Sinks.Many<Boolean>
|
|||||||
"app.pseudonymize.generator=BUILDIN"
|
"app.pseudonymize.generator=BUILDIN"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(name = "configsUpdateProducer", types = [MockSink::class])
|
@MockBean(name = "configsUpdateProducer", classes = [MockSink::class])
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [
|
|
||||||
Generator::class,
|
Generator::class,
|
||||||
MtbFileSender::class,
|
MtbFileSender::class,
|
||||||
RequestProcessor::class,
|
RequestProcessor::class,
|
||||||
TransformationService::class,
|
TransformationService::class,
|
||||||
GPasConnectionCheckService::class,
|
GPasConnectionCheckService::class,
|
||||||
RestConnectionCheckService::class,
|
RestConnectionCheckService::class,
|
||||||
GIcsConnectionCheckService::class
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
class ConfigControllerTest {
|
class ConfigControllerTest {
|
||||||
|
|
||||||
@ -147,10 +143,8 @@ class ConfigControllerTest {
|
|||||||
"app.security.admin-user=admin"
|
"app.security.admin-user=admin"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [
|
|
||||||
TokenService::class
|
TokenService::class
|
||||||
]
|
|
||||||
)
|
)
|
||||||
inner class WithTokensEnabled {
|
inner class WithTokensEnabled {
|
||||||
private lateinit var tokenService: TokenService
|
private lateinit var tokenService: TokenService
|
||||||
@ -184,13 +178,7 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShouldNotSaveTokenWithExstingName() {
|
fun testShouldNotSaveTokenWithExstingName() {
|
||||||
whenever(tokenService.addToken(anyString())).thenReturn(
|
whenever(tokenService.addToken(anyString())).thenReturn(Result.failure(RuntimeException("Testfailure")))
|
||||||
Result.failure(
|
|
||||||
RuntimeException(
|
|
||||||
"Testfailure"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
mockMvc.post("/configs/tokens") {
|
mockMvc.post("/configs/tokens") {
|
||||||
with(user("admin").roles("ADMIN"))
|
with(user("admin").roles("ADMIN"))
|
||||||
@ -264,10 +252,8 @@ class ConfigControllerTest {
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [
|
|
||||||
UserRoleService::class
|
UserRoleService::class
|
||||||
]
|
|
||||||
)
|
)
|
||||||
inner class WithUserRolesEnabled {
|
inner class WithUserRolesEnabled {
|
||||||
private lateinit var userRoleService: UserRoleService
|
private lateinit var userRoleService: UserRoleService
|
||||||
@ -311,10 +297,7 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
val idCaptor = argumentCaptor<Long>()
|
val idCaptor = argumentCaptor<Long>()
|
||||||
val roleCaptor = argumentCaptor<Role>()
|
val roleCaptor = argumentCaptor<Role>()
|
||||||
verify(userRoleService, times(1)).updateUserRole(
|
verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture())
|
||||||
idCaptor.capture(),
|
|
||||||
roleCaptor.capture()
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(idCaptor.firstValue).isEqualTo(42)
|
assertThat(idCaptor.firstValue).isEqualTo(42)
|
||||||
assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN)
|
assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN)
|
||||||
@ -352,23 +335,20 @@ class ConfigControllerTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
applicationContext: WebApplicationContext
|
applicationContext: WebApplicationContext,
|
||||||
) {
|
) {
|
||||||
this.webClient = MockMvcWebTestClient
|
this.webClient = MockMvcWebTestClient
|
||||||
.bindToApplicationContext(applicationContext).build()
|
.bindToApplicationContext(applicationContext).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShouldRequestGPasSSE() {
|
fun testShouldRequestSSE() {
|
||||||
val expectedEvent =
|
val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
|
||||||
ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
|
|
||||||
|
|
||||||
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
|
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
|
||||||
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
|
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
|
||||||
|
|
||||||
val result =
|
val result = webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM).exchange()
|
||||||
webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM)
|
|
||||||
.exchange()
|
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
||||||
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
|
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.*
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
@ -28,8 +30,6 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.services.RequestService
|
import dev.dnpm.etl.processor.services.RequestService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.htmlunit.html.HtmlPage
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -40,13 +40,13 @@ import org.mockito.kotlin.any
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.PageImpl
|
import org.springframework.data.domain.PageImpl
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.security.test.context.support.WithMockUser
|
import org.springframework.security.test.context.support.WithMockUser
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
@ -71,8 +71,8 @@ import java.util.*
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [RequestService::class]
|
RequestService::class
|
||||||
)
|
)
|
||||||
class HomeControllerTest {
|
class HomeControllerTest {
|
||||||
|
|
||||||
|
@ -19,21 +19,21 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.htmlunit.html.HtmlPage
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
@ -56,8 +56,8 @@ import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
|
|||||||
"app.security.enable-tokens=true"
|
"app.security.enable-tokens=true"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [TokenService::class]
|
TokenService::class,
|
||||||
)
|
)
|
||||||
class LoginControllerTest {
|
class LoginControllerTest {
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
@ -41,10 +41,10 @@ import org.mockito.kotlin.doAnswer
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
@ -74,8 +74,8 @@ import java.time.temporal.ChronoUnit
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [RequestService::class]
|
RequestService::class
|
||||||
)
|
)
|
||||||
class StatisticsRestControllerTest {
|
class StatisticsRestControllerTest {
|
||||||
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class ConsentCheckFileBased implements ICheckConsent{
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ConsentCheckFileBased.class);
|
|
||||||
|
|
||||||
public ConsentCheckFileBased() {
|
|
||||||
log.info("ConsentCheckFileBased initialized...");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TtpConsentStatus getTtpConsentStatus(String personIdentifierValue) {
|
|
||||||
return TtpConsentStatus.UNKNOWN_CHECK_FILE;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
public enum ConsentDomain {
|
|
||||||
BroadConsent,
|
|
||||||
Modelvorhaben64e
|
|
||||||
}
|
|
@ -1,281 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
|
||||||
import ca.uhn.fhir.parser.DataFormatException;
|
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig;
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties;
|
|
||||||
import java.util.Date;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
|
||||||
import org.hl7.fhir.r4.model.BooleanType;
|
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
|
||||||
import org.hl7.fhir.r4.model.Coding;
|
|
||||||
import org.hl7.fhir.r4.model.DateType;
|
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome;
|
|
||||||
import org.hl7.fhir.r4.model.Parameters;
|
|
||||||
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.retry.TerminatedRetryException;
|
|
||||||
import org.springframework.retry.support.RetryTemplate;
|
|
||||||
import org.springframework.web.client.RestClientException;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
|
||||||
|
|
||||||
|
|
||||||
public class GicsConsentService implements ICheckConsent {
|
|
||||||
|
|
||||||
private final Logger log = LoggerFactory.getLogger(GicsConsentService.class);
|
|
||||||
|
|
||||||
private final GIcsConfigProperties gIcsConfigProperties;
|
|
||||||
|
|
||||||
public static final String IS_CONSENTED_ENDPOINT = "/$isConsented";
|
|
||||||
public static final String IS_POLICY_STATES_FOR_PERSON_ENDPOINT = "/$currentPolicyStatesForPerson";
|
|
||||||
private final RetryTemplate retryTemplate;
|
|
||||||
private final RestTemplate restTemplate;
|
|
||||||
private final FhirContext fhirContext;
|
|
||||||
private final HttpHeaders httpHeader;
|
|
||||||
private String url;
|
|
||||||
|
|
||||||
public GicsConsentService(GIcsConfigProperties gIcsConfigProperties,
|
|
||||||
RetryTemplate retryTemplate, RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
|
|
||||||
this.gIcsConfigProperties = gIcsConfigProperties;
|
|
||||||
this.retryTemplate = retryTemplate;
|
|
||||||
this.restTemplate = restTemplate;
|
|
||||||
this.fhirContext = appFhirConfig.fhirContext();
|
|
||||||
httpHeader = buildHeader(gIcsConfigProperties.getUsername(),
|
|
||||||
gIcsConfigProperties.getPassword());
|
|
||||||
log.info("GicsConsentService initialized...");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGicsUri(String endpoint) {
|
|
||||||
if (url == null) {
|
|
||||||
final String gIcsBaseUri = gIcsConfigProperties.getUri();
|
|
||||||
if (StringUtils.isBlank(gIcsBaseUri)) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"gICS base URL is empty - should call gICS with false configuration.");
|
|
||||||
}
|
|
||||||
url = UriComponentsBuilder.fromUriString(gIcsBaseUri).path(IS_CONSENTED_ENDPOINT)
|
|
||||||
.toUriString();
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static HttpHeaders buildHeader(String gPasUserName, String gPasPassword) {
|
|
||||||
var headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_XML);
|
|
||||||
|
|
||||||
if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) {
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.setBasicAuth(gPasUserName, gPasPassword);
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static Parameters getIsConsentedRequestParam(GIcsConfigProperties configProperties,
|
|
||||||
String personIdentifierValue) {
|
|
||||||
var result = new Parameters();
|
|
||||||
result.addParameter(new ParametersParameterComponent().setName("personIdentifier").setValue(
|
|
||||||
new Identifier().setValue(personIdentifierValue)
|
|
||||||
.setSystem(configProperties.getPersonIdentifierSystem())));
|
|
||||||
result.addParameter(new ParametersParameterComponent().setName("domain")
|
|
||||||
.setValue(new StringType().setValue(configProperties.getBroadConsentDomainName())));
|
|
||||||
result.addParameter(new ParametersParameterComponent().setName("policy").setValue(
|
|
||||||
new Coding().setCode(configProperties.getPolicyCode())
|
|
||||||
.setSystem(configProperties.getPolicySystem())));
|
|
||||||
|
|
||||||
/*
|
|
||||||
* is mandatory parameter, but we ignore it via additional configuration parameter
|
|
||||||
* 'ignoreVersionNumber'.
|
|
||||||
*/
|
|
||||||
result.addParameter(new ParametersParameterComponent().setName("version")
|
|
||||||
.setValue(new StringType().setValue("1.1")));
|
|
||||||
|
|
||||||
/* add config parameter with:
|
|
||||||
* ignoreVersionNumber -> true ->> Reason is we cannot know which policy version each patient
|
|
||||||
* has possibly signed or not, therefore we are happy with any version found.
|
|
||||||
* unknownStateIsConsideredAsDecline -> true
|
|
||||||
*/
|
|
||||||
var config = new ParametersParameterComponent().setName("config").addPart(
|
|
||||||
new ParametersParameterComponent().setName("ignoreVersionNumber")
|
|
||||||
.setValue(new BooleanType().setValue(true))).addPart(
|
|
||||||
new ParametersParameterComponent().setName("unknownStateIsConsideredAsDecline")
|
|
||||||
.setValue(new BooleanType().setValue(false)));
|
|
||||||
result.addParameter(config);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String callGicsApi(Parameters parameter, String endpoint) {
|
|
||||||
var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter);
|
|
||||||
|
|
||||||
HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader);
|
|
||||||
ResponseEntity<String> responseEntity;
|
|
||||||
try {
|
|
||||||
var url = getGicsUri(endpoint);
|
|
||||||
|
|
||||||
responseEntity = retryTemplate.execute(
|
|
||||||
ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class));
|
|
||||||
} catch (RestClientException e) {
|
|
||||||
var msg = String.format("Get consents status request failed reason: '%s",
|
|
||||||
e.getMessage());
|
|
||||||
log.error(msg);
|
|
||||||
return null;
|
|
||||||
|
|
||||||
} catch (TerminatedRetryException terminatedRetryException) {
|
|
||||||
var msg = String.format(
|
|
||||||
"Get consents status process has been terminated. termination reason: '%s",
|
|
||||||
terminatedRetryException.getMessage());
|
|
||||||
log.error(msg);
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
|
||||||
return responseEntity.getBody();
|
|
||||||
} else {
|
|
||||||
var msg = String.format(
|
|
||||||
"Trusted party system reached but request failed! code: '%s' response: '%s'",
|
|
||||||
responseEntity.getStatusCode(), responseEntity.getBody());
|
|
||||||
log.error(msg);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TtpConsentStatus getTtpConsentStatus(String personIdentifierValue) {
|
|
||||||
var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties,
|
|
||||||
personIdentifierValue);
|
|
||||||
|
|
||||||
var consentStatusResponse = callGicsApi(parameter,
|
|
||||||
GicsConsentService.IS_CONSENTED_ENDPOINT);
|
|
||||||
return evaluateConsentResponse(consentStatusResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bundle currentConsentForPersonAndTemplate(String personIdentifierValue,
|
|
||||||
ConsentDomain targetConsentDomain, Date requestDate) {
|
|
||||||
|
|
||||||
String consentDomain = getConsentDomain(targetConsentDomain);
|
|
||||||
|
|
||||||
var requestParameter = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(
|
|
||||||
gIcsConfigProperties, personIdentifierValue, requestDate, consentDomain);
|
|
||||||
|
|
||||||
var consentDataSerialized = callGicsApi(requestParameter,
|
|
||||||
GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT);
|
|
||||||
|
|
||||||
if (consentDataSerialized == null) {
|
|
||||||
// error occurred - should not process further!
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"consent data request failed - stopping processing! - try again or fix other problems first.");
|
|
||||||
}
|
|
||||||
IBaseResource iBaseResource = fhirContext.newXmlParser()
|
|
||||||
.parseResource(consentDataSerialized);
|
|
||||||
if (iBaseResource instanceof OperationOutcome) {
|
|
||||||
// log error - very likely a configuration error
|
|
||||||
String errorMessage =
|
|
||||||
"Consent request failed! Check outcome:\n " + consentDataSerialized;
|
|
||||||
log.error(errorMessage);
|
|
||||||
throw new IllegalStateException(errorMessage);
|
|
||||||
} else if (iBaseResource instanceof Bundle) {
|
|
||||||
return (Bundle) iBaseResource;
|
|
||||||
} else {
|
|
||||||
String errorMessage = "Consent request failed! Unexpected response received! -> "
|
|
||||||
+ consentDataSerialized;
|
|
||||||
log.error(errorMessage);
|
|
||||||
throw new IllegalStateException(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private String getConsentDomain(ConsentDomain targetConsentDomain) {
|
|
||||||
String consentDomain;
|
|
||||||
switch (targetConsentDomain) {
|
|
||||||
case BroadConsent -> {
|
|
||||||
consentDomain = gIcsConfigProperties.getBroadConsentDomainName();
|
|
||||||
}
|
|
||||||
case Modelvorhaben64e -> {
|
|
||||||
consentDomain = gIcsConfigProperties.getGnomDeConsentDomainName();
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"target ConsentDomain is missing but must be provided!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return consentDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bundle getBroadConsent(String personIdentifierValue, Date requestDate) {
|
|
||||||
return currentConsentForPersonAndTemplate(personIdentifierValue, ConsentDomain.BroadConsent,
|
|
||||||
requestDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) {
|
|
||||||
return currentConsentForPersonAndTemplate(personIdentifierValue,
|
|
||||||
ConsentDomain.Modelvorhaben64e, requestDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson(
|
|
||||||
GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate,
|
|
||||||
String targetDomain) {
|
|
||||||
var requestParameter = new Parameters();
|
|
||||||
requestParameter.addParameter(new ParametersParameterComponent().setName("personIdentifier")
|
|
||||||
.setValue(new Identifier().setValue(personIdentifierValue)
|
|
||||||
.setSystem(gIcsConfigProperties.getPersonIdentifierSystem())));
|
|
||||||
|
|
||||||
requestParameter.addParameter(new ParametersParameterComponent().setName("domain")
|
|
||||||
.setValue(new StringType().setValue(targetDomain)));
|
|
||||||
|
|
||||||
Parameters nestedConfigParameters = new Parameters();
|
|
||||||
nestedConfigParameters.addParameter(
|
|
||||||
new ParametersParameterComponent().setName("idMatchingType").setValue(
|
|
||||||
new Coding().setSystem(
|
|
||||||
"https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType")
|
|
||||||
.setCode("AT_LEAST_ONE"))).addParameter("ignoreVersionNumber", false)
|
|
||||||
.addParameter("unknownStateIsConsideredAsDecline", false)
|
|
||||||
.addParameter("requestDate", new DateType().setValue(requestDate));
|
|
||||||
|
|
||||||
requestParameter.addParameter(new ParametersParameterComponent().setName("config").addPart()
|
|
||||||
.setResource(nestedConfigParameters));
|
|
||||||
|
|
||||||
return requestParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TtpConsentStatus evaluateConsentResponse(String consentStatusResponse) {
|
|
||||||
if (consentStatusResponse == null) {
|
|
||||||
return TtpConsentStatus.FAILED_TO_ASK;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var response = fhirContext.newJsonParser().parseResource(consentStatusResponse);
|
|
||||||
|
|
||||||
if (response instanceof Parameters responseParameters) {
|
|
||||||
|
|
||||||
var responseValue = responseParameters.getParameter("consented").getValue();
|
|
||||||
var isConsented = responseValue.castToBoolean(responseValue);
|
|
||||||
if (!isConsented.hasValue()) {
|
|
||||||
return TtpConsentStatus.FAILED_TO_ASK;
|
|
||||||
}
|
|
||||||
if (isConsented.booleanValue()) {
|
|
||||||
return TtpConsentStatus.CONSENTED;
|
|
||||||
} else {
|
|
||||||
return TtpConsentStatus.CONSENT_MISSING_OR_REJECTED;
|
|
||||||
}
|
|
||||||
} else if (response instanceof OperationOutcome outcome) {
|
|
||||||
log.error("failed to get consent status from ttp. probably configuration error. "
|
|
||||||
+ "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome));
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (DataFormatException dfe) {
|
|
||||||
log.error("failed to parse response to FHIR R4 resource.", dfe);
|
|
||||||
}
|
|
||||||
return TtpConsentStatus.FAILED_TO_ASK;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
|
|
||||||
public interface ICheckConsent {
|
|
||||||
|
|
||||||
TtpConsentStatus getTtpConsentStatus(String personIdentifierValue);
|
|
||||||
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
public enum TtpConsentStatus {
|
|
||||||
/**
|
|
||||||
* Valid consent found
|
|
||||||
*/
|
|
||||||
CONSENTED,
|
|
||||||
|
|
||||||
CONSENT_MISSING_OR_REJECTED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Due technical problems consent status is unknown
|
|
||||||
*/
|
|
||||||
FAILED_TO_ASK,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent status is validate via file property 'consent.status'
|
|
||||||
*/
|
|
||||||
UNKNOWN_CHECK_FILE
|
|
||||||
}
|
|
@ -21,7 +21,6 @@ package dev.dnpm.etl.processor.pseudonym;
|
|||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.parser.IParser;
|
import ca.uhn.fhir.parser.IParser;
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig;
|
|
||||||
import dev.dnpm.etl.processor.config.GPasConfigProperties;
|
import dev.dnpm.etl.processor.config.GPasConfigProperties;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
@ -37,7 +36,7 @@ import org.springframework.web.client.RestTemplate;
|
|||||||
|
|
||||||
public class GpasPseudonymGenerator implements Generator {
|
public class GpasPseudonymGenerator implements Generator {
|
||||||
|
|
||||||
private final FhirContext r4Context;
|
private final static FhirContext r4Context = FhirContext.forR4();
|
||||||
private final String gPasUrl;
|
private final String gPasUrl;
|
||||||
private final String psnTargetDomain;
|
private final String psnTargetDomain;
|
||||||
private final HttpHeaders httpHeader;
|
private final HttpHeaders httpHeader;
|
||||||
@ -46,13 +45,11 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
|
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) {
|
||||||
RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
|
|
||||||
this.retryTemplate = retryTemplate;
|
this.retryTemplate = retryTemplate;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
this.gPasUrl = gpasCfg.getUri();
|
this.gPasUrl = gpasCfg.getUri();
|
||||||
this.psnTargetDomain = gpasCfg.getTarget();
|
this.psnTargetDomain = gpasCfg.getTarget();
|
||||||
this.r4Context = appFhirConfig.fhirContext();
|
|
||||||
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
|
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
|
||||||
|
|
||||||
log.debug(String.format("%s has been initialized", this.getClass().getName()));
|
log.debug(String.format("%s has been initialized", this.getClass().getName()));
|
||||||
@ -100,6 +97,7 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
return psnValue.replaceAll(forbiddenCharsRegex, "_");
|
return psnValue.replaceAll(forbiddenCharsRegex, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody) {
|
protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody) {
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -21,10 +21,16 @@ package dev.dnpm.etl.processor.config
|
|||||||
|
|
||||||
import dev.dnpm.etl.processor.security.Role
|
import dev.dnpm.etl.processor.security.Role
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty
|
||||||
|
|
||||||
@ConfigurationProperties(AppConfigProperties.NAME)
|
@ConfigurationProperties(AppConfigProperties.NAME)
|
||||||
data class AppConfigProperties(
|
data class AppConfigProperties(
|
||||||
var bwhcUri: String?,
|
var bwhcUri: String?,
|
||||||
|
@get:DeprecatedConfigurationProperty(
|
||||||
|
reason = "Deprecated in favor of 'app.pseudonymize.generator'",
|
||||||
|
replacement = "app.pseudonymize.generator"
|
||||||
|
)
|
||||||
|
var pseudonymizer: PseudonymGenerator = PseudonymGenerator.BUILDIN,
|
||||||
var transformations: List<TransformationProperties> = listOf(),
|
var transformations: List<TransformationProperties> = listOf(),
|
||||||
var maxRetryAttempts: Int = 3,
|
var maxRetryAttempts: Int = 3,
|
||||||
var duplicationDetection: Boolean = true
|
var duplicationDetection: Boolean = true
|
||||||
@ -50,60 +56,16 @@ data class GPasConfigProperties(
|
|||||||
val target: String = "etl-processor",
|
val target: String = "etl-processor",
|
||||||
val username: String?,
|
val username: String?,
|
||||||
val password: String?,
|
val password: String?,
|
||||||
|
@get:DeprecatedConfigurationProperty(
|
||||||
|
reason = "Deprecated in favor of including Root CA"
|
||||||
|
)
|
||||||
|
val sslCaLocation: String?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NAME = "app.pseudonymize.gpas"
|
const val NAME = "app.pseudonymize.gpas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConfigurationProperties(GIcsConfigProperties.NAME)
|
|
||||||
data class GIcsConfigProperties(
|
|
||||||
/**
|
|
||||||
* Base URL to gICS System
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
val uri: String?,
|
|
||||||
val username: String?,
|
|
||||||
val password: String?,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If value is 'true' valid consent at processing time is mandatory for transmission of DNPM
|
|
||||||
* files otherwise they will be flagged and skipped.
|
|
||||||
* If value 'false' or missing consent status is assumed to be valid.
|
|
||||||
*/
|
|
||||||
val enabled: Boolean?,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gICS specific system
|
|
||||||
* **/
|
|
||||||
val personIdentifierSystem: String =
|
|
||||||
"https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain of broad consent resources
|
|
||||||
**/
|
|
||||||
val broadConsentDomainName: String = "MII",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain of Modelvorhaben 64e consent resources
|
|
||||||
**/
|
|
||||||
val gnomDeConsentDomainName: String = "GenomDE_MV",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value to expect in case of positiv consent
|
|
||||||
*/
|
|
||||||
val policyCode: String = "2.16.840.1.113883.3.1937.777.24.5.3.6",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent Policy which should be used for consent check
|
|
||||||
*/
|
|
||||||
val policySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3"
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val NAME = "app.consent.gics"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConfigurationProperties(RestTargetProperties.NAME)
|
@ConfigurationProperties(RestTargetProperties.NAME)
|
||||||
data class RestTargetProperties(
|
data class RestTargetProperties(
|
||||||
val uri: String?,
|
val uri: String?,
|
||||||
@ -120,8 +82,18 @@ data class RestTargetProperties(
|
|||||||
data class KafkaProperties(
|
data class KafkaProperties(
|
||||||
val inputTopic: String?,
|
val inputTopic: String?,
|
||||||
val outputTopic: String = "etl-processor",
|
val outputTopic: String = "etl-processor",
|
||||||
|
@get:DeprecatedConfigurationProperty(
|
||||||
|
reason = "Deprecated",
|
||||||
|
replacement = "outputTopic"
|
||||||
|
)
|
||||||
|
val topic: String = outputTopic,
|
||||||
val outputResponseTopic: String = "${outputTopic}_response",
|
val outputResponseTopic: String = "${outputTopic}_response",
|
||||||
val groupId: String = "${outputTopic}_group",
|
@get:DeprecatedConfigurationProperty(
|
||||||
|
reason = "Deprecated",
|
||||||
|
replacement = "outputResponseTopic"
|
||||||
|
)
|
||||||
|
val responseTopic: String = outputResponseTopic,
|
||||||
|
val groupId: String = "${topic}_group",
|
||||||
val servers: String = ""
|
val servers: String = ""
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -20,10 +20,10 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
package dev.dnpm.etl.processor.config
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import dev.dnpm.etl.processor.consent.ConsentCheckFileBased
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
import dev.dnpm.etl.processor.consent.ICheckConsent
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.monitoring.*
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
||||||
@ -32,6 +32,12 @@ import dev.dnpm.etl.processor.security.TokenRepository
|
|||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.Transformation
|
import dev.dnpm.etl.processor.services.Transformation
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
|
import org.apache.hc.client5.http.impl.classic.HttpClients
|
||||||
|
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager
|
||||||
|
import org.apache.hc.client5.http.socket.ConnectionSocketFactory
|
||||||
|
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory
|
||||||
|
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory
|
||||||
|
import org.apache.hc.core5.http.config.RegistryBuilder
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
@ -39,6 +45,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
|
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
|
||||||
import org.springframework.retry.RetryCallback
|
import org.springframework.retry.RetryCallback
|
||||||
import org.springframework.retry.RetryContext
|
import org.springframework.retry.RetryContext
|
||||||
import org.springframework.retry.RetryListener
|
import org.springframework.retry.RetryListener
|
||||||
@ -51,6 +58,13 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
|||||||
import org.springframework.web.client.HttpClientErrorException
|
import org.springframework.web.client.HttpClientErrorException
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
@ -60,8 +74,7 @@ import kotlin.time.toJavaDuration
|
|||||||
value = [
|
value = [
|
||||||
AppConfigProperties::class,
|
AppConfigProperties::class,
|
||||||
PseudonymizeConfigProperties::class,
|
PseudonymizeConfigProperties::class,
|
||||||
GPasConfigProperties::class,
|
GPasConfigProperties::class
|
||||||
GIcsConfigProperties::class
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@ -74,15 +87,22 @@ class AppConfiguration {
|
|||||||
return RestTemplate()
|
return RestTemplate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun appFhirConfig(): AppFhirConfig{
|
|
||||||
return AppFhirConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
||||||
@Bean
|
@Bean
|
||||||
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): Generator {
|
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
||||||
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig)
|
try {
|
||||||
|
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
||||||
|
return GpasPseudonymGenerator(
|
||||||
|
configProperties,
|
||||||
|
retryTemplate,
|
||||||
|
createCustomGpasRestTemplate(configProperties)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
|
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
|
||||||
@ -91,6 +111,92 @@ class AppConfiguration {
|
|||||||
return AnonymizingGenerator()
|
return AnonymizingGenerator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@Bean
|
||||||
|
fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
||||||
|
try {
|
||||||
|
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
||||||
|
return GpasPseudonymGenerator(
|
||||||
|
configProperties,
|
||||||
|
retryTemplate,
|
||||||
|
createCustomGpasRestTemplate(configProperties)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCustomGpasRestTemplate(configProperties: GPasConfigProperties): RestTemplate {
|
||||||
|
fun getSslContext(certificateLocation: String): SSLContext? {
|
||||||
|
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||||
|
|
||||||
|
val fis = FileInputStream(certificateLocation)
|
||||||
|
val ca = CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(BufferedInputStream(fis)) as X509Certificate
|
||||||
|
|
||||||
|
ks.load(null, null)
|
||||||
|
ks.setCertificateEntry(1.toString(), ca)
|
||||||
|
|
||||||
|
val tmf = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm()
|
||||||
|
)
|
||||||
|
tmf.init(ks)
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(null, tmf.trustManagers, null)
|
||||||
|
|
||||||
|
return sslContext
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCustomRestTemplate(customSslContext: SSLContext): RestTemplate {
|
||||||
|
val sslsf = SSLConnectionSocketFactory(customSslContext)
|
||||||
|
val socketFactoryRegistry = RegistryBuilder.create<ConnectionSocketFactory>()
|
||||||
|
.register("https", sslsf).register("http", PlainConnectionSocketFactory()).build()
|
||||||
|
|
||||||
|
val connectionManager = BasicHttpClientConnectionManager(
|
||||||
|
socketFactoryRegistry
|
||||||
|
)
|
||||||
|
val httpClient = HttpClients.custom()
|
||||||
|
.setConnectionManager(connectionManager).build()
|
||||||
|
|
||||||
|
val requestFactory = HttpComponentsClientHttpRequestFactory(
|
||||||
|
httpClient
|
||||||
|
)
|
||||||
|
return RestTemplate(requestFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
||||||
|
val customSslContext = getSslContext(configProperties.sslCaLocation)
|
||||||
|
logger.warn(
|
||||||
|
String.format(
|
||||||
|
"%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.",
|
||||||
|
this.javaClass.name, configProperties.sslCaLocation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (customSslContext != null) {
|
||||||
|
return getCustomRestTemplate(customSslContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw RuntimeException("Custom SSL configuration for gPAS not usable")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "BUILDIN")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@Bean
|
||||||
|
fun buildinPseudonymGeneratorOnDeprecatedProperty(): Generator {
|
||||||
|
return AnonymizingGenerator()
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun pseudonymizeService(
|
fun pseudonymizeService(
|
||||||
generator: Generator,
|
generator: Generator,
|
||||||
@ -100,21 +206,17 @@ class AppConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun reportService(): ReportService {
|
fun reportService(objectMapper: ObjectMapper): ReportService {
|
||||||
return ReportService(getObjectMapper())
|
return ReportService(objectMapper)
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun getObjectMapper () : ObjectMapper{
|
|
||||||
return JacksonConfig().objectMapper()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun transformationService(
|
fun transformationService(
|
||||||
|
objectMapper: ObjectMapper,
|
||||||
configProperties: AppConfigProperties
|
configProperties: AppConfigProperties
|
||||||
): TransformationService {
|
): TransformationService {
|
||||||
logger.info("Apply ${configProperties.transformations.size} transformation rules")
|
logger.info("Apply ${configProperties.transformations.size} transformation rules")
|
||||||
return TransformationService(getObjectMapper(), configProperties.transformations.map {
|
return TransformationService(objectMapper, configProperties.transformations.map {
|
||||||
Transformation.of(it.path) from it.from to it.to
|
Transformation.of(it.path) from it.from to it.to
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -180,33 +282,5 @@ class AppConfiguration {
|
|||||||
fun jdbcConfiguration(): AbstractJdbcConfiguration {
|
fun jdbcConfiguration(): AbstractJdbcConfiguration {
|
||||||
return AppJdbcConfiguration()
|
return AppJdbcConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
|
|
||||||
fun gicsConsentService( gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): ICheckConsent {
|
|
||||||
return GicsConsentService(
|
|
||||||
gIcsConfigProperties,
|
|
||||||
retryTemplate,
|
|
||||||
restTemplate,
|
|
||||||
appFhirConfig
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
|
|
||||||
@Bean
|
|
||||||
fun gIcsConnectionCheckService(
|
|
||||||
restTemplate: RestTemplate,
|
|
||||||
gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
|
||||||
): ConnectionCheckService {
|
|
||||||
return GIcsConnectionCheckService(restTemplate, gIcsConfigProperties, connectionCheckUpdateProducer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
fun constService(): ICheckConsent {
|
|
||||||
return ConsentCheckFileBased()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class AppFhirConfig {
|
|
||||||
private val fhirCtx: FhirContext = FhirContext.forR4()
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun fhirContext(): FhirContext {
|
|
||||||
return fhirCtx
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -71,7 +71,7 @@ class AppKafkaConfiguration {
|
|||||||
kafkaProperties: KafkaProperties,
|
kafkaProperties: KafkaProperties,
|
||||||
kafkaResponseProcessor: KafkaResponseProcessor
|
kafkaResponseProcessor: KafkaResponseProcessor
|
||||||
): KafkaMessageListenerContainer<String, String> {
|
): KafkaMessageListenerContainer<String, String> {
|
||||||
val containerProperties = ContainerProperties(kafkaProperties.outputResponseTopic)
|
val containerProperties = ContainerProperties(kafkaProperties.responseTopic)
|
||||||
containerProperties.messageListener = kafkaResponseProcessor
|
containerProperties.messageListener = kafkaResponseProcessor
|
||||||
return KafkaMessageListenerContainer(consumerFactory, containerProperties)
|
return KafkaMessageListenerContainer(consumerFactory, containerProperties)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -87,14 +87,9 @@ class AppSecurityConfiguration(
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
||||||
fun filterChainOidc(
|
fun filterChainOidc(http: HttpSecurity, passwordEncoder: PasswordEncoder, userRoleRepository: UserRoleRepository, sessionRegistry: SessionRegistry): SecurityFilterChain {
|
||||||
http: HttpSecurity,
|
|
||||||
passwordEncoder: PasswordEncoder,
|
|
||||||
userRoleRepository: UserRoleRepository,
|
|
||||||
sessionRegistry: SessionRegistry
|
|
||||||
): SecurityFilterChain {
|
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeRequests {
|
||||||
authorize("/configs/**", hasRole("ADMIN"))
|
authorize("/configs/**", hasRole("ADMIN"))
|
||||||
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
|
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
|
||||||
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
||||||
@ -132,22 +127,13 @@ class AppSecurityConfiguration(
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
||||||
fun grantedAuthoritiesMapper(
|
fun grantedAuthoritiesMapper(userRoleRepository: UserRoleRepository, appSecurityConfigProperties: SecurityConfigProperties): GrantedAuthoritiesMapper {
|
||||||
userRoleRepository: UserRoleRepository,
|
|
||||||
appSecurityConfigProperties: SecurityConfigProperties
|
|
||||||
): GrantedAuthoritiesMapper {
|
|
||||||
return GrantedAuthoritiesMapper { grantedAuthority ->
|
return GrantedAuthoritiesMapper { grantedAuthority ->
|
||||||
grantedAuthority.filterIsInstance<OidcUserAuthority>()
|
grantedAuthority.filterIsInstance<OidcUserAuthority>()
|
||||||
.onEach {
|
.onEach {
|
||||||
val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername)
|
val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername)
|
||||||
if (userRole.isEmpty) {
|
if (userRole.isEmpty) {
|
||||||
userRoleRepository.save(
|
userRoleRepository.save(UserRole(null, it.userInfo.preferredUsername, appSecurityConfigProperties.defaultNewUserRole))
|
||||||
UserRole(
|
|
||||||
null,
|
|
||||||
it.userInfo.preferredUsername,
|
|
||||||
appSecurityConfigProperties.defaultNewUserRole
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
@ -161,7 +147,7 @@ class AppSecurityConfiguration(
|
|||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true)
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true)
|
||||||
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeRequests {
|
||||||
authorize("/configs/**", hasRole("ADMIN"))
|
authorize("/configs/**", hasRole("ADMIN"))
|
||||||
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
|
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
|
||||||
authorize("/report/**", hasRole("ADMIN"))
|
authorize("/report/**", hasRole("ADMIN"))
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource
|
|
||||||
|
|
||||||
class FhirResourceModule : SimpleModule() {
|
|
||||||
init {
|
|
||||||
addSerializer(IBaseResource::class.java, IBaseResourceSerializer())
|
|
||||||
addDeserializer(IBaseResource::class.java, IBaseResourceDeserializer())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext
|
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource
|
|
||||||
|
|
||||||
class IBaseResourceDeserializer : JsonDeserializer<IBaseResource>() {
|
|
||||||
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): IBaseResource {
|
|
||||||
val fhirContext = FhirContext.forR4()
|
|
||||||
|
|
||||||
val jsonNode = p?.readValueAsTree<JsonNode>()
|
|
||||||
val json = jsonNode?.toString()
|
|
||||||
|
|
||||||
return fhirContext.newJsonParser().parseResource(json) as IBaseResource
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer
|
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource
|
|
||||||
|
|
||||||
class IBaseResourceSerializer : JsonSerializer<IBaseResource>() {
|
|
||||||
override fun serialize(
|
|
||||||
value: IBaseResource,
|
|
||||||
gen: JsonGenerator,
|
|
||||||
serializers: SerializerProvider
|
|
||||||
) {
|
|
||||||
val fhirContext = FhirContext.forR4()
|
|
||||||
val json = fhirContext.newJsonParser().encodeResourceToString(value)
|
|
||||||
gen.writeRawValue(json)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class JacksonConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun objectMapper(): ObjectMapper =
|
|
||||||
ObjectMapper()
|
|
||||||
.registerModule(FhirResourceModule())
|
|
||||||
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).registerModule(
|
|
||||||
JavaTimeModule()
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -22,14 +22,11 @@ package dev.dnpm.etl.processor.input
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.kafka.listener.MessageListener
|
import org.springframework.kafka.listener.MessageListener
|
||||||
|
|
||||||
class KafkaInputListener(
|
class KafkaInputListener(
|
||||||
@ -38,29 +35,10 @@ class KafkaInputListener(
|
|||||||
) : MessageListener<String, String> {
|
) : MessageListener<String, String> {
|
||||||
private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java)
|
private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java)
|
||||||
|
|
||||||
override fun onMessage(record: ConsumerRecord<String, String>) {
|
override fun onMessage(data: ConsumerRecord<String, String>) {
|
||||||
when (guessMimeType(record)) {
|
val mtbFile = objectMapper.readValue(data.value(), MtbFile::class.java)
|
||||||
MediaType.APPLICATION_JSON_VALUE -> handleBwhcMessage(record)
|
|
||||||
CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE -> handleDnpmV2Message(record)
|
|
||||||
else -> {
|
|
||||||
/* ignore other messages */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessMimeType(record: ConsumerRecord<String, String>): String {
|
|
||||||
if (record.headers().headers("contentType").toList().isEmpty()) {
|
|
||||||
// Fallback if no contentType set (old behavior)
|
|
||||||
return MediaType.APPLICATION_JSON_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
return record.headers().headers("contentType")?.firstOrNull()?.value().contentToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleBwhcMessage(record: ConsumerRecord<String, String>) {
|
|
||||||
val mtbFile = objectMapper.readValue(record.value(), MtbFile::class.java)
|
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
val firstRequestIdHeader = record.headers().headers("requestId")?.firstOrNull()
|
val firstRequestIdHeader = data.headers().headers("requestId")?.firstOrNull()
|
||||||
val requestId = if (null != firstRequestIdHeader) {
|
val requestId = if (null != firstRequestIdHeader) {
|
||||||
RequestId(String(firstRequestIdHeader.value()))
|
RequestId(String(firstRequestIdHeader.value()))
|
||||||
} else {
|
} else {
|
||||||
@ -77,20 +55,10 @@ class KafkaInputListener(
|
|||||||
} else {
|
} else {
|
||||||
logger.debug("Accepted MTB File and process deletion")
|
logger.debug("Accepted MTB File and process deletion")
|
||||||
if (requestId.isBlank()) {
|
if (requestId.isBlank()) {
|
||||||
requestProcessor.processDeletion(patientId, TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
requestProcessor.processDeletion(patientId)
|
||||||
} else {
|
} else {
|
||||||
requestProcessor.processDeletion(
|
requestProcessor.processDeletion(patientId, requestId)
|
||||||
patientId,
|
|
||||||
requestId,
|
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDnpmV2Message(record: ConsumerRecord<String, String>) {
|
|
||||||
// Do not handle DNPM-V2 for now
|
|
||||||
logger.warn("Ignoring MTB File in DNPM V2 format: Not implemented yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -21,21 +21,16 @@ package dev.dnpm.etl.processor.input
|
|||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.dnpm.etl.processor.consent.ICheckConsent
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(path = ["mtbfile", "mtb"])
|
@RequestMapping(path = ["mtbfile", "mtb"])
|
||||||
class MtbFileRestController(
|
class MtbFileRestController(
|
||||||
private val requestProcessor: RequestProcessor, private val iCheckConsent: ICheckConsent
|
private val requestProcessor: RequestProcessor,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
|
||||||
@ -45,49 +40,23 @@ class MtbFileRestController(
|
|||||||
return ResponseEntity.ok("Test")
|
return ResponseEntity.ok("Test")
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
|
@PostMapping
|
||||||
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
||||||
val consentStatusBooleanPair = checkConsentStatus(mtbFile)
|
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
||||||
val ttpConsentStatus = consentStatusBooleanPair.first
|
logger.debug("Accepted MTB File for processing")
|
||||||
val isConsentOK = consentStatusBooleanPair.second
|
|
||||||
if (isConsentOK) {
|
|
||||||
logger.debug("Accepted MTB File (bwHC V1) for processing")
|
|
||||||
requestProcessor.processMtbFile(mtbFile)
|
requestProcessor.processMtbFile(mtbFile)
|
||||||
} else {
|
} else {
|
||||||
|
logger.debug("Accepted MTB File and process deletion")
|
||||||
logger.debug("Accepted MTB File (bwHC V1) and process deletion")
|
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
requestProcessor.processDeletion(patientId, ttpConsentStatus)
|
requestProcessor.processDeletion(patientId)
|
||||||
}
|
}
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkConsentStatus(mtbFile: MtbFile): Pair<TtpConsentStatus, Boolean> {
|
|
||||||
var ttpConsentStatus = iCheckConsent.getTtpConsentStatus(mtbFile.patient.id)
|
|
||||||
|
|
||||||
val isConsentOK =
|
|
||||||
(ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.ACTIVE) ||
|
|
||||||
ttpConsentStatus.equals(
|
|
||||||
TtpConsentStatus.CONSENTED
|
|
||||||
)
|
|
||||||
if (ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.REJECTED) {
|
|
||||||
// in case ttp check is disabled - we propagate rejected status anyway
|
|
||||||
ttpConsentStatus = TtpConsentStatus.CONSENT_MISSING_OR_REJECTED
|
|
||||||
}
|
|
||||||
return Pair(ttpConsentStatus, isConsentOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = [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()
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping(path = ["{patientId}"])
|
@DeleteMapping(path = ["{patientId}"])
|
||||||
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
|
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
|
||||||
logger.debug("Accepted patient ID to process deletion")
|
logger.debug("Accepted patient ID to process deletion")
|
||||||
requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
requestProcessor.processDeletion(PatientId(patientId))
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.monitoring
|
package dev.dnpm.etl.processor.monitoring
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import jakarta.annotation.PostConstruct
|
import jakarta.annotation.PostConstruct
|
||||||
@ -69,12 +68,6 @@ sealed class ConnectionCheckResult {
|
|||||||
override val timestamp: Instant,
|
override val timestamp: Instant,
|
||||||
override val lastChange: Instant
|
override val lastChange: Instant
|
||||||
) : ConnectionCheckResult()
|
) : ConnectionCheckResult()
|
||||||
|
|
||||||
data class GIcsConnectionCheckResult(
|
|
||||||
override val available: Boolean,
|
|
||||||
override val timestamp: Instant,
|
|
||||||
override val lastChange: Instant
|
|
||||||
) : ConnectionCheckResult()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class KafkaConnectionCheckService(
|
class KafkaConnectionCheckService(
|
||||||
@ -215,56 +208,3 @@ class GPasConnectionCheckService(
|
|||||||
return this.result
|
return this.result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GIcsConnectionCheckService(
|
|
||||||
private val restTemplate: RestTemplate,
|
|
||||||
private val gIcsConfigProperties: GIcsConfigProperties,
|
|
||||||
@Qualifier("connectionCheckUpdateProducer")
|
|
||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
|
||||||
) : ConnectionCheckService {
|
|
||||||
|
|
||||||
private var result = ConnectionCheckResult.GIcsConnectionCheckResult(false, Instant.now(), Instant.now())
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
@Scheduled(cron = "0 * * * * *")
|
|
||||||
fun check() {
|
|
||||||
result = try {
|
|
||||||
|
|
||||||
val uri = UriComponentsBuilder.fromUriString(
|
|
||||||
gIcsConfigProperties.uri.toString()).path("/metadata").build().toUri()
|
|
||||||
|
|
||||||
val headers = HttpHeaders()
|
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
|
||||||
if (!gIcsConfigProperties.username.isNullOrBlank() && !gIcsConfigProperties.password.isNullOrBlank()) {
|
|
||||||
headers.setBasicAuth(gIcsConfigProperties.username, gIcsConfigProperties.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
val available = restTemplate.exchange(
|
|
||||||
uri,
|
|
||||||
HttpMethod.GET,
|
|
||||||
HttpEntity<Void>(headers),
|
|
||||||
Void::class.java
|
|
||||||
).statusCode == HttpStatus.OK
|
|
||||||
|
|
||||||
ConnectionCheckResult.GIcsConnectionCheckResult(
|
|
||||||
available,
|
|
||||||
Instant.now(),
|
|
||||||
if (result.available == available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
ConnectionCheckResult.GIcsConnectionCheckResult(
|
|
||||||
false,
|
|
||||||
Instant.now(),
|
|
||||||
if (!result.available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
connectionCheckUpdateProducer.emitNext(
|
|
||||||
result,
|
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult {
|
|
||||||
return this.result
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,6 +24,5 @@ enum class RequestStatus(val value: String) {
|
|||||||
WARNING("warning"),
|
WARNING("warning"),
|
||||||
ERROR("error"),
|
ERROR("error"),
|
||||||
UNKNOWN("unknown"),
|
UNKNOWN("unknown"),
|
||||||
DUPLICATION("duplication"),
|
DUPLICATION("duplication")
|
||||||
NO_CONSENT("no-consent")
|
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -22,12 +22,10 @@ package dev.dnpm.etl.processor.output
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.apache.kafka.clients.producer.ProducerRecord
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.kafka.core.KafkaTemplate
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
|
|
||||||
@ -40,20 +38,14 @@ class KafkaMtbFileSender(
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
||||||
|
|
||||||
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val record =
|
val result = kafkaTemplate.send(
|
||||||
ProducerRecord(kafkaProperties.outputTopic, key(request), objectMapper.writeValueAsString(request))
|
kafkaProperties.topic,
|
||||||
when (request) {
|
key(request),
|
||||||
is BwhcV1MtbFileRequest -> record.headers()
|
objectMapper.writeValueAsString(Data(request.requestId, request.mtbFile))
|
||||||
.add("contentType", MediaType.APPLICATION_JSON_VALUE.toByteArray())
|
)
|
||||||
|
|
||||||
is DnpmV2MtbFileRequest -> record.headers()
|
|
||||||
.add("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = kafkaTemplate.send(record)
|
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent file via KafkaMtbFileSender")
|
logger.debug("Sent file via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@ -67,7 +59,7 @@ class KafkaMtbFileSender(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
||||||
val dummyMtbFile = MtbFile.builder()
|
val dummyMtbFile = MtbFile.builder()
|
||||||
.withConsent(
|
.withConsent(
|
||||||
Consent.builder()
|
Consent.builder()
|
||||||
@ -79,15 +71,12 @@ class KafkaMtbFileSender(
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val record =
|
val result = kafkaTemplate.send(
|
||||||
ProducerRecord(
|
kafkaProperties.topic,
|
||||||
kafkaProperties.outputTopic,
|
|
||||||
key(request),
|
key(request),
|
||||||
// Always use old BwhcV1FileRequest with Consent REJECT
|
objectMapper.writeValueAsString(Data(request.requestId, dummyMtbFile))
|
||||||
objectMapper.writeValueAsString(BwhcV1MtbFileRequest(request.requestId, dummyMtbFile))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = kafkaTemplate.send(record)
|
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@ -102,15 +91,16 @@ class KafkaMtbFileSender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun endpoint(): String {
|
override fun endpoint(): String {
|
||||||
return "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})"
|
return "${this.kafkaProperties.servers} (${this.kafkaProperties.topic}/${this.kafkaProperties.responseTopic})"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun key(request: MtbRequest): String {
|
private fun key(request: MtbFileSender.MtbFileRequest): String {
|
||||||
return when (request) {
|
return "{\"pid\": \"${request.mtbFile.patient.id}\"}"
|
||||||
is BwhcV1MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}"
|
|
||||||
is DnpmV2MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}"
|
|
||||||
is DeleteRequest -> "{\"pid\": \"${request.patientId.value}\"}"
|
|
||||||
else -> throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun key(request: MtbFileSender.DeleteRequest): String {
|
||||||
|
return "{\"pid\": \"${request.patientId.value}\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Data(val requestId: RequestId, val content: MtbFile)
|
||||||
}
|
}
|
@ -19,17 +19,25 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.springframework.http.HttpStatusCode
|
import org.springframework.http.HttpStatusCode
|
||||||
|
|
||||||
interface MtbFileSender {
|
interface MtbFileSender {
|
||||||
fun <T> send(request: MtbFileRequest<T>): Response
|
fun send(request: MtbFileRequest): Response
|
||||||
|
|
||||||
fun send(request: DeleteRequest): Response
|
fun send(request: DeleteRequest): Response
|
||||||
|
|
||||||
fun endpoint(): String
|
fun endpoint(): String
|
||||||
|
|
||||||
data class Response(val status: RequestStatus, val body: String = "")
|
data class Response(val status: RequestStatus, val body: String = "")
|
||||||
|
|
||||||
|
data class MtbFileRequest(val requestId: RequestId, val mtbFile: MtbFile)
|
||||||
|
|
||||||
|
data class DeleteRequest(val requestId: RequestId, val patientId: PatientPseudonym)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.asRequestStatus(): RequestStatus {
|
fun Int.asRequestStatus(): RequestStatus {
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
|
||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
|
|
||||||
interface MtbRequest {
|
|
||||||
val requestId: RequestId
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface MtbFileRequest<out T> : MtbRequest {
|
|
||||||
override val requestId: RequestId
|
|
||||||
val content: T
|
|
||||||
|
|
||||||
fun patientPseudonym(): PatientPseudonym
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BwhcV1MtbFileRequest(
|
|
||||||
override val requestId: RequestId,
|
|
||||||
override val content: MtbFile
|
|
||||||
) : MtbFileRequest<MtbFile> {
|
|
||||||
override fun patientPseudonym(): PatientPseudonym {
|
|
||||||
return PatientPseudonym(content.patient.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DnpmV2MtbFileRequest(
|
|
||||||
override val requestId: RequestId,
|
|
||||||
override val content: Mtb
|
|
||||||
) : MtbFileRequest<Mtb> {
|
|
||||||
override fun patientPseudonym(): PatientPseudonym {
|
|
||||||
return PatientPseudonym(content.patient.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeleteRequest(
|
|
||||||
override val requestId: RequestId,
|
|
||||||
val patientId: PatientPseudonym
|
|
||||||
) : MtbRequest
|
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,11 +19,10 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
@ -47,11 +46,11 @@ abstract class RestMtbFileSender(
|
|||||||
|
|
||||||
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
||||||
|
|
||||||
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders(request)
|
val headers = getHttpHeaders()
|
||||||
val entityReq = HttpEntity(request.content, headers)
|
val entityReq = HttpEntity(request.mtbFile, headers)
|
||||||
val response = restTemplate.postForEntity(
|
val response = restTemplate.postForEntity(
|
||||||
sendUrl(),
|
sendUrl(),
|
||||||
entityReq,
|
entityReq,
|
||||||
@ -77,10 +76,10 @@ abstract class RestMtbFileSender(
|
|||||||
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders(request)
|
val headers = getHttpHeaders()
|
||||||
val entityReq = HttpEntity(null, headers)
|
val entityReq = HttpEntity(null, headers)
|
||||||
restTemplate.delete(
|
restTemplate.delete(
|
||||||
deleteUrl(request.patientId),
|
deleteUrl(request.patientId),
|
||||||
@ -103,15 +102,11 @@ abstract class RestMtbFileSender(
|
|||||||
return this.restTargetProperties.uri.orEmpty()
|
return this.restTargetProperties.uri.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHttpHeaders(request: MtbRequest): HttpHeaders {
|
private fun getHttpHeaders(): HttpHeaders {
|
||||||
val username = restTargetProperties.username
|
val username = restTargetProperties.username
|
||||||
val password = restTargetProperties.password
|
val password = restTargetProperties.password
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = when (request) {
|
headers.contentType = MediaType.APPLICATION_JSON
|
||||||
is BwhcV1MtbFileRequest -> MediaType.APPLICATION_JSON
|
|
||||||
is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
|
||||||
else -> MediaType.APPLICATION_JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
||||||
return headers
|
return headers
|
||||||
|
@ -21,13 +21,12 @@ package dev.dnpm.etl.processor.pseudonym
|
|||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
import org.hl7.fhir.r4.model.Consent
|
|
||||||
|
|
||||||
/** Replaces patient ID with generated patient pseudonym
|
/** Replaces patient ID with generated patient pseudonym
|
||||||
*
|
*
|
||||||
* @param pseudonymizeService The pseudonymizeService to be used
|
* @param pseudonymizeService The pseudonymizeService to be used
|
||||||
|
*
|
||||||
* @return The MTB file containing patient pseudonymes
|
* @return The MTB file containing patient pseudonymes
|
||||||
*/
|
*/
|
||||||
infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
||||||
@ -50,11 +49,7 @@ infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
|||||||
}
|
}
|
||||||
this.lastGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
this.lastGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
||||||
this.molecularPathologyFindings?.forEach { it.patient = patientPseudonym }
|
this.molecularPathologyFindings?.forEach { it.patient = patientPseudonym }
|
||||||
this.molecularTherapies?.forEach { molecularTherapy ->
|
this.molecularTherapies?.forEach { molecularTherapy -> molecularTherapy.history.forEach { it.patient = patientPseudonym } }
|
||||||
molecularTherapy.history.forEach {
|
|
||||||
it.patient = patientPseudonym
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.ngsReports?.forEach { it.patient = patientPseudonym }
|
this.ngsReports?.forEach { it.patient = patientPseudonym }
|
||||||
this.previousGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
this.previousGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
||||||
this.rebiopsyRequests?.forEach { it.patient = patientPseudonym }
|
this.rebiopsyRequests?.forEach { it.patient = patientPseudonym }
|
||||||
@ -68,6 +63,7 @@ infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
|||||||
* Creates new hash of content IDs with given prefix except for patient IDs
|
* Creates new hash of content IDs with given prefix except for patient IDs
|
||||||
*
|
*
|
||||||
* @param pseudonymizeService The pseudonymizeService to be used
|
* @param pseudonymizeService The pseudonymizeService to be used
|
||||||
|
*
|
||||||
* @return The MTB file containing rehashed content IDs
|
* @return The MTB file containing rehashed content IDs
|
||||||
*/
|
*/
|
||||||
infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
||||||
@ -228,100 +224,3 @@ infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replaces patient ID with generated patient pseudonym
|
|
||||||
*
|
|
||||||
* @since 0.11.0
|
|
||||||
*
|
|
||||||
* @param pseudonymizeService The pseudonymizeService to be used
|
|
||||||
* @return The MTB file containing patient pseudonymes
|
|
||||||
*/
|
|
||||||
infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
|
||||||
val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value
|
|
||||||
|
|
||||||
this.episodesOfCare?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.carePlans?.forEach {
|
|
||||||
it.patient.id = patientPseudonym
|
|
||||||
it.rebiopsyRequests?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.histologyReevaluationRequests?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.medicationRecommendations.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.studyEnrollmentRecommendations?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.procedureRecommendations?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.geneticCounselingRecommendation.patient.id = patientPseudonym
|
|
||||||
}
|
|
||||||
this.diagnoses?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.guidelineTherapies?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.guidelineProcedures?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.patient.id = patientPseudonym
|
|
||||||
this.claims?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.claimResponses?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.diagnoses?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.histologyReports?.forEach {
|
|
||||||
it.patient.id = patientPseudonym
|
|
||||||
it.results.tumorMorphology?.patient?.id = patientPseudonym
|
|
||||||
it.results.tumorCellContent?.patient?.id = patientPseudonym
|
|
||||||
}
|
|
||||||
this.ngsReports?.forEach {
|
|
||||||
it.patient.id = patientPseudonym
|
|
||||||
it.results.simpleVariants?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.results.copyNumberVariants?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.results.dnaFusions?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.results.rnaFusions?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.results.tumorCellContent?.patient?.id = patientPseudonym
|
|
||||||
it.results.brcaness?.patient?.id = patientPseudonym
|
|
||||||
it.results.tmb?.patient?.id = patientPseudonym
|
|
||||||
it.results.hrdScore?.patient?.id = patientPseudonym
|
|
||||||
}
|
|
||||||
this.ihcReports?.forEach {
|
|
||||||
it.patient.id = patientPseudonym
|
|
||||||
it.results.msiMmr?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
it.results.proteinExpression?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
}
|
|
||||||
this.responses?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.specimens?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.priorDiagnosticReports?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.performanceStatus?.forEach { it.patient.id = patientPseudonym }
|
|
||||||
this.systemicTherapies?.forEach {
|
|
||||||
it.history?.forEach {
|
|
||||||
it.patient.id = patientPseudonym
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.followUps?.forEach {
|
|
||||||
it.patient.id = patientPseudonym
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: MUST CREATE TESTCASE - NEEDS TESTING!!
|
|
||||||
this.metadata?.researchConsents?.forEach { it -> {
|
|
||||||
val consent = it as? Consent
|
|
||||||
consent?.patient?.reference = "Patient/$patientPseudonym"
|
|
||||||
consent?.patient?.display = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates new hash of content IDs with given prefix except for patient IDs
|
|
||||||
*
|
|
||||||
* @since 0.11.0
|
|
||||||
*
|
|
||||||
* @param pseudonymizeService The pseudonymizeService to be used
|
|
||||||
* @return The MTB file containing rehashed content IDs
|
|
||||||
*/
|
|
||||||
infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
|
||||||
val prefix = pseudonymizeService.prefix()
|
|
||||||
|
|
||||||
fun anonymize(id: String): String {
|
|
||||||
val hash = DigestUtils.sha256Hex("$prefix-$id").substring(0, 41).lowercase()
|
|
||||||
return "$prefix$hash"
|
|
||||||
}
|
|
||||||
|
|
||||||
this.episodesOfCare?.forEach {
|
|
||||||
it?.apply {
|
|
||||||
id = id?.let {
|
|
||||||
anonymize(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO all other properties
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -23,27 +23,16 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.*
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Report
|
import dev.dnpm.etl.processor.monitoring.Report
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.output.*
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith
|
import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith
|
||||||
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
|
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
|
||||||
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.MvhMetadata
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.Provision
|
|
||||||
import org.apache.commons.codec.binary.Base32
|
import org.apache.commons.codec.binary.Base32
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource
|
|
||||||
import org.hl7.fhir.r4.model.Bundle
|
|
||||||
import org.hl7.fhir.r4.model.Consent
|
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -57,8 +46,7 @@ class RequestProcessor(
|
|||||||
private val requestService: RequestService,
|
private val requestService: RequestService,
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||||
private val appConfigProperties: AppConfigProperties,
|
private val appConfigProperties: AppConfigProperties
|
||||||
private val gicsConsentService: GicsConsentService?
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: MtbFile) {
|
fun processMtbFile(mtbFile: MtbFile) {
|
||||||
@ -67,112 +55,29 @@ class RequestProcessor(
|
|||||||
|
|
||||||
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
val pid = PatientId(mtbFile.patient.id)
|
||||||
|
|
||||||
mtbFile pseudonymizeWith pseudonymizeService
|
mtbFile pseudonymizeWith pseudonymizeService
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
mtbFile anonymizeContentWith pseudonymizeService
|
||||||
val request = BwhcV1MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
|
||||||
saveAndSend(request, pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: Mtb) {
|
val request = MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
||||||
processMtbFile(mtbFile, randomRequestId())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
val patientPseudonym = PatientPseudonym(request.mtbFile.patient.id)
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
|
||||||
|
|
||||||
addConsentToMtb(mtbFile)
|
|
||||||
mtbFile pseudonymizeWith pseudonymizeService
|
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
|
||||||
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
|
||||||
saveAndSend(request, pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addConsentToMtb(mtbFile: Mtb) {
|
|
||||||
if (gicsConsentService == null) return
|
|
||||||
// init metadata if necessary
|
|
||||||
if (mtbFile.metadata == null) {
|
|
||||||
val mvhMetadata = MvhMetadata.builder().build();
|
|
||||||
mtbFile.metadata = mvhMetadata
|
|
||||||
if (mtbFile.metadata.researchConsents == null) {
|
|
||||||
mtbFile.metadata.researchConsents = mutableListOf()
|
|
||||||
}
|
|
||||||
if (mtbFile.metadata.modelProjectConsent == null) {
|
|
||||||
mtbFile.metadata.modelProjectConsent = ModelProjectConsent()
|
|
||||||
mtbFile.metadata.modelProjectConsent.provisions = mutableListOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fixme Date should be extracted from mtbFile
|
|
||||||
val consentGnomeDe =
|
|
||||||
gicsConsentService.getGenomDeConsent(mtbFile.patient.id, Date.from(Instant.now()))
|
|
||||||
addGenomeDbProvisions(mtbFile, consentGnomeDe)
|
|
||||||
|
|
||||||
// fixme Date should be extracted from mtbFile
|
|
||||||
val broadConsent =
|
|
||||||
gicsConsentService.getBroadConsent(mtbFile.patient.id, Date.from(Instant.now()))
|
|
||||||
embedBroadConsentResources(mtbFile, broadConsent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun embedBroadConsentResources(
|
|
||||||
mtbFile: Mtb,
|
|
||||||
broadConsent: Bundle
|
|
||||||
) {
|
|
||||||
broadConsent.entry.forEach { it ->
|
|
||||||
mtbFile.metadata.researchConsents.add(mapOf(it.resource.id to it as IBaseResource))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addGenomeDbProvisions(
|
|
||||||
mtbFile: Mtb,
|
|
||||||
consentGnomeDe: Bundle
|
|
||||||
) {
|
|
||||||
consentGnomeDe.entry.forEach { it ->
|
|
||||||
{
|
|
||||||
val consent = it.resource as Consent
|
|
||||||
val provisionComponent = consent.provision.provision.firstOrNull()
|
|
||||||
val provisionCode =
|
|
||||||
provisionComponent?.code?.firstOrNull()?.coding?.firstOrNull()?.code
|
|
||||||
var isValidCode = true
|
|
||||||
if (provisionCode != null) {
|
|
||||||
var modelProjectConsentPurpose: ModelProjectConsentPurpose =
|
|
||||||
ModelProjectConsentPurpose.SEQUENCING
|
|
||||||
if (provisionCode == "Teilnahme") {
|
|
||||||
modelProjectConsentPurpose = ModelProjectConsentPurpose.SEQUENCING
|
|
||||||
} else if (provisionCode == "Fallidentifizierung") {
|
|
||||||
modelProjectConsentPurpose = ModelProjectConsentPurpose.CASE_IDENTIFICATION
|
|
||||||
} else if (provisionCode == "Rekontaktierung") {
|
|
||||||
modelProjectConsentPurpose = ModelProjectConsentPurpose.REIDENTIFICATION
|
|
||||||
} else {
|
|
||||||
isValidCode = false
|
|
||||||
}
|
|
||||||
if (isValidCode) mtbFile.metadata.modelProjectConsent.provisions.add(
|
|
||||||
Provision.builder().type(
|
|
||||||
ConsentProvision.forValue(provisionComponent.type.name)
|
|
||||||
).date(provisionComponent.period.start).purpose(
|
|
||||||
modelProjectConsentPurpose
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
|
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
request.requestId,
|
requestId,
|
||||||
request.patientPseudonym(),
|
patientPseudonym,
|
||||||
pid,
|
pid,
|
||||||
fingerprint(request),
|
fingerprint(request.mtbFile),
|
||||||
RequestType.MTB_FILE,
|
RequestType.MTB_FILE,
|
||||||
RequestStatus.UNKNOWN
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (appConfigProperties.duplicationDetection && isDuplication(request)) {
|
if (appConfigProperties.duplicationDetection && isDuplication(mtbFile)) {
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
request.requestId,
|
requestId,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
RequestStatus.DUPLICATION
|
RequestStatus.DUPLICATION
|
||||||
)
|
)
|
||||||
@ -184,7 +89,7 @@ class RequestProcessor(
|
|||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
request.requestId,
|
requestId,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
responseStatus.status,
|
responseStatus.status,
|
||||||
when (responseStatus.status) {
|
when (responseStatus.status) {
|
||||||
@ -195,38 +100,26 @@ class RequestProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean {
|
private fun isDuplication(pseudonymizedMtbFile: MtbFile): Boolean {
|
||||||
val patientPseudonym = when (pseudonymizedMtbFileRequest) {
|
val patientPseudonym = PatientPseudonym(pseudonymizedMtbFile.patient.id)
|
||||||
is BwhcV1MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
|
||||||
is DnpmV2MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastMtbFileRequestForPatient =
|
val lastMtbFileRequestForPatient =
|
||||||
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
||||||
val isLastRequestDeletion =
|
val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
||||||
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
|
||||||
|
|
||||||
return null != lastMtbFileRequestForPatient
|
return null != lastMtbFileRequestForPatient
|
||||||
&& !isLastRequestDeletion
|
&& !isLastRequestDeletion
|
||||||
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(
|
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFile)
|
||||||
pseudonymizedMtbFileRequest
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) {
|
fun processDeletion(patientId: PatientId) {
|
||||||
processDeletion(patientId, randomRequestId(), isConsented)
|
processDeletion(patientId, randomRequestId())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) {
|
fun processDeletion(patientId: PatientId, requestId: RequestId) {
|
||||||
try {
|
try {
|
||||||
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
|
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
|
||||||
|
|
||||||
val requestStatus: RequestStatus = when (isConsented) {
|
|
||||||
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED -> RequestStatus.NO_CONSENT
|
|
||||||
TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR
|
|
||||||
TtpConsentStatus.CONSENTED, TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
requestId,
|
requestId,
|
||||||
@ -234,11 +127,11 @@ class RequestProcessor(
|
|||||||
patientId,
|
patientId,
|
||||||
fingerprint(patientPseudonym.value),
|
fingerprint(patientPseudonym.value),
|
||||||
RequestType.DELETE,
|
RequestType.DELETE,
|
||||||
requestStatus
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym))
|
val responseStatus = sender.send(MtbFileSender.DeleteRequest(requestId, patientPseudonym))
|
||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
@ -267,11 +160,8 @@ class RequestProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint {
|
private fun fingerprint(mtbFile: MtbFile): Fingerprint {
|
||||||
return when (request) {
|
return fingerprint(objectMapper.writeValueAsString(mtbFile))
|
||||||
is BwhcV1MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content))
|
|
||||||
is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fingerprint(s: String): Fingerprint {
|
private fun fingerprint(s: String): Fingerprint {
|
||||||
|
@ -70,12 +70,6 @@ class ResponseProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestStatus.NO_CONSENT -> {
|
|
||||||
it.report = Report(
|
|
||||||
"Einwilligung Status fehlt, widerrufen oder ungeklärt."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
logger.error("Cannot process response: Unknown response!")
|
logger.error("Cannot process response: Unknown response!")
|
||||||
return@ifPresentOrElse
|
return@ifPresentOrElse
|
||||||
|
@ -23,21 +23,10 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import com.jayway.jsonpath.JsonPath
|
import com.jayway.jsonpath.JsonPath
|
||||||
import com.jayway.jsonpath.PathNotFoundException
|
import com.jayway.jsonpath.PathNotFoundException
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
|
|
||||||
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
||||||
fun transform(mtbFile: MtbFile): MtbFile {
|
fun transform(mtbFile: MtbFile): MtbFile {
|
||||||
val json = transform(objectMapper.writeValueAsString(mtbFile))
|
var json = objectMapper.writeValueAsString(mtbFile)
|
||||||
return objectMapper.readValue(json, MtbFile::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun transform(mtbFile: Mtb): Mtb {
|
|
||||||
val json = transform(objectMapper.writeValueAsString(mtbFile))
|
|
||||||
return objectMapper.readValue(json, Mtb::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun transform(content: String): String {
|
|
||||||
var json = content
|
|
||||||
|
|
||||||
transformations.forEach { transformation ->
|
transformations.forEach { transformation ->
|
||||||
val jsonPath = JsonPath.parse(json)
|
val jsonPath = JsonPath.parse(json)
|
||||||
@ -59,7 +48,7 @@ class TransformationService(private val objectMapper: ObjectMapper, private val
|
|||||||
json = jsonPath.jsonString()
|
json = jsonPath.jsonString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return json
|
return objectMapper.readValue(json, MtbFile::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTransformations(): List<Transformation> {
|
fun getTransformations(): List<Transformation> {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor
|
package dev.dnpm.etl.processor
|
||||||
|
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Fingerprint(val value: String) {
|
class Fingerprint(val value: String) {
|
||||||
@ -48,16 +47,3 @@ value class PatientId(val value: String)
|
|||||||
value class PatientPseudonym(val value: String)
|
value class PatientPseudonym(val value: String)
|
||||||
|
|
||||||
fun emptyPatientPseudonym() = PatientPseudonym("")
|
fun emptyPatientPseudonym() = PatientPseudonym("")
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom MediaTypes
|
|
||||||
*
|
|
||||||
* @since 0.11.0
|
|
||||||
*/
|
|
||||||
object CustomMediaType {
|
|
||||||
val APPLICATION_VND_DNPM_V2_MTB_JSON = MediaType("application", "vnd.dnpm.v2.mtb+json")
|
|
||||||
const val APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE = "application/vnd.dnpm.v2.mtb+json"
|
|
||||||
|
|
||||||
val APPLICATION_VND_DNPM_V2_RD_JSON = MediaType("application", "vnd.dnpm.v2.rd+json")
|
|
||||||
const val APPLICATION_VND_DNPM_V2_RD_JSON_VALUE = "application/vnd.dnpm.v2.rd+json"
|
|
||||||
}
|
|
||||||
|
@ -19,7 +19,10 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.monitoring.*
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
||||||
|
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
||||||
|
import dev.dnpm.etl.processor.monitoring.OutputConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
import dev.dnpm.etl.processor.security.Role
|
import dev.dnpm.etl.processor.security.Role
|
||||||
@ -58,15 +61,11 @@ class ConfigController(
|
|||||||
val gPasConnectionAvailable =
|
val gPasConnectionAvailable =
|
||||||
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
||||||
|
|
||||||
val gIcsConnectionAvailable =
|
|
||||||
connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
|
||||||
|
|
||||||
model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName)
|
model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName)
|
||||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
||||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
||||||
model.addAttribute("outputConnectionAvailable", outputConnectionAvailable)
|
model.addAttribute("outputConnectionAvailable", outputConnectionAvailable)
|
||||||
model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable)
|
model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable)
|
||||||
model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable)
|
|
||||||
model.addAttribute("tokensEnabled", tokenService != null)
|
model.addAttribute("tokensEnabled", tokenService != null)
|
||||||
if (tokenService != null) {
|
if (tokenService != null) {
|
||||||
model.addAttribute("tokens", tokenService.findAll())
|
model.addAttribute("tokens", tokenService.findAll())
|
||||||
@ -120,24 +119,6 @@ class ConfigController(
|
|||||||
return "configs/gPasConnectionAvailable"
|
return "configs/gPasConnectionAvailable"
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(params = ["gIcsConnectionAvailable"])
|
|
||||||
fun gIcsConnectionAvailable(model: Model): String {
|
|
||||||
val gIcsConnectionAvailable =
|
|
||||||
connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
|
||||||
|
|
||||||
model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName)
|
|
||||||
model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint())
|
|
||||||
model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable)
|
|
||||||
if (tokenService != null) {
|
|
||||||
model.addAttribute("tokensEnabled", true)
|
|
||||||
model.addAttribute("tokens", tokenService.findAll())
|
|
||||||
} else {
|
|
||||||
model.addAttribute("tokens", listOf<Token>())
|
|
||||||
}
|
|
||||||
|
|
||||||
return "configs/gIcsConnectionAvailable"
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(path = ["tokens"])
|
@PostMapping(path = ["tokens"])
|
||||||
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
|
fun addToken(@ModelAttribute("name") name: String, model: Model): String {
|
||||||
if (tokenService == null) {
|
if (tokenService == null) {
|
||||||
@ -209,7 +190,6 @@ class ConfigController(
|
|||||||
is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check"
|
is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check"
|
||||||
is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check"
|
is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check"
|
||||||
is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check"
|
is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check"
|
||||||
is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerSentEvent.builder<Any>()
|
ServerSentEvent.builder<Any>()
|
||||||
|
@ -49,11 +49,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
|
||||||
<div th:insert="~{configs/gIcsConnectionAvailable.html}" th:hx-get="@{/configs?gIcsConnectionAvailable}" hx-trigger="sse:gics-connection-check">
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
<section hx-ext="sse" th:sse-connect="@{/configs/events}">
|
||||||
<div th:insert="~{configs/outputConnectionAvailable.html}" th:hx-get="@{/configs?outputConnectionAvailable}" hx-trigger="sse:output-connection-check">
|
<div th:insert="~{configs/outputConnectionAvailable.html}" th:hx-get="@{/configs?outputConnectionAvailable}" hx-trigger="sse:output-connection-check">
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
<th:block th:if="${gIcsConnectionAvailable == null}">
|
|
||||||
<h2><span>🟦</span> gICS nicht konfiguriert - Einwilligung wird über Dateiinhalt geprüft</h2>
|
|
||||||
</th:block>
|
|
||||||
<th:block th:if="${gIcsConnectionAvailable != null}">
|
|
||||||
<h2><span th:if="${gIcsConnectionAvailable.available}">✅</span><span th:if="${not(gIcsConnectionAvailable.available)}">⚡</span> Verbindung zu gICS</h2>
|
|
||||||
<div>
|
|
||||||
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gIcsConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(gIcsConnectionAvailable.timestamp)}"></time>
|
|
||||||
|
|
|
||||||
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gIcsConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(gIcsConnectionAvailable.lastChange)}"></time>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Die Verbindung ist aktuell</span>
|
|
||||||
<strong th:if="${gIcsConnectionAvailable.available}" style="color: green">verfügbar.</strong>
|
|
||||||
<strong th:if="${not(gIcsConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
|
|
||||||
</div>
|
|
||||||
<div class="connection-display border">
|
|
||||||
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
|
||||||
<span class="connection" th:classappend="${gIcsConnectionAvailable.available ? 'available' : ''}"></span>
|
|
||||||
<img th:src="@{/server.png}" alt="gICS" />
|
|
||||||
<span>ETL-Processor</span>
|
|
||||||
<span></span>
|
|
||||||
<span>gICS</span>
|
|
||||||
</div>
|
|
||||||
</th:block>
|
|
@ -1,124 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.consent;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
|
||||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration;
|
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig;
|
|
||||||
import dev.dnpm.etl.processor.config.GIcsConfigProperties;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Date;
|
|
||||||
import org.hl7.fhir.r4.model.BooleanType;
|
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome.IssueType;
|
|
||||||
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent;
|
|
||||||
import org.hl7.fhir.r4.model.Parameters;
|
|
||||||
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
|
||||||
import org.springframework.test.context.TestPropertySource;
|
|
||||||
import org.springframework.test.web.client.MockRestServiceServer;
|
|
||||||
|
|
||||||
|
|
||||||
@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class})
|
|
||||||
@TestPropertySource(properties = {"app.consent.gics.enabled=true",
|
|
||||||
"app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"})
|
|
||||||
@RestClientTest
|
|
||||||
public class GicsConsentServiceTest {
|
|
||||||
|
|
||||||
public static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics";
|
|
||||||
@Autowired
|
|
||||||
MockRestServiceServer mockRestServiceServer;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
GicsConsentService gicsConsentService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
AppConfiguration appConfiguration;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
AppFhirConfig appFhirConfig;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
GIcsConfigProperties gIcsConfigProperties;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() {
|
|
||||||
mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getTtpConsentStatus() {
|
|
||||||
final Parameters responseConsented = new Parameters().addParameter(
|
|
||||||
new ParametersParameterComponent().setName("consented")
|
|
||||||
.setValue(new BooleanType().setValue(true)));
|
|
||||||
|
|
||||||
mockRestServiceServer.expect(
|
|
||||||
requestTo("http://localhost:8090/ttp-fhir/fhir/gics"
|
|
||||||
+ GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
|
|
||||||
withSuccess(appFhirConfig.fhirContext().newJsonParser()
|
|
||||||
.encodeResourceToString(responseConsented),
|
|
||||||
MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
var consentStatus = gicsConsentService.getTtpConsentStatus("123456");
|
|
||||||
assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void consentRevoced() {
|
|
||||||
final Parameters responseRevoced = new Parameters().addParameter(
|
|
||||||
new ParametersParameterComponent().setName("consented")
|
|
||||||
.setValue(new BooleanType().setValue(false)));
|
|
||||||
|
|
||||||
mockRestServiceServer.expect(
|
|
||||||
requestTo("http://localhost:8090/ttp-fhir/fhir/gics"
|
|
||||||
+ GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
|
|
||||||
withSuccess(appFhirConfig.fhirContext().newJsonParser()
|
|
||||||
.encodeResourceToString(responseRevoced),
|
|
||||||
MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
var consentStatus = gicsConsentService.getTtpConsentStatus("123456");
|
|
||||||
assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void gicsParameterInvalid() {
|
|
||||||
final OperationOutcome responseErrorOutcome = new OperationOutcome().addIssue(
|
|
||||||
new OperationOutcomeIssueComponent().setSeverity(
|
|
||||||
IssueSeverity.ERROR).setCode(IssueType.PROCESSING)
|
|
||||||
.setDiagnostics("Invalid policy parameter..."));
|
|
||||||
|
|
||||||
mockRestServiceServer.expect(
|
|
||||||
requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
|
|
||||||
withSuccess(appFhirConfig.fhirContext().newJsonParser()
|
|
||||||
.encodeResourceToString(responseErrorOutcome),
|
|
||||||
MediaType.APPLICATION_JSON));
|
|
||||||
|
|
||||||
var consentStatus = gicsConsentService.getTtpConsentStatus("123456");
|
|
||||||
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void buildRequestParameterCurrentPolicyStatesForPersonTest() {
|
|
||||||
|
|
||||||
String pid = "12345678";
|
|
||||||
var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(gIcsConfigProperties,
|
|
||||||
pid, Date.from(Instant.now()),gIcsConfigProperties.getGnomDeConsentDomainName());
|
|
||||||
|
|
||||||
assertThat(result.getParameter().size()).as("should contain 3 parameter resources").isEqualTo(3);
|
|
||||||
|
|
||||||
assertThat(((StringType)result.getParameter("domain").getValue()).getValue()).isEqualTo(gIcsConfigProperties.getGnomDeConsentDomainName());
|
|
||||||
assertThat(((Identifier)result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo(pid);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -23,8 +23,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
import de.ukw.ccc.bwhc.dto.Patient
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import org.apache.kafka.clients.consumer.ConsumerRecord
|
import org.apache.kafka.clients.consumer.ConsumerRecord
|
||||||
import org.apache.kafka.common.header.internals.RecordHeader
|
import org.apache.kafka.common.header.internals.RecordHeader
|
||||||
@ -35,7 +33,10 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.anyValueClass
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
@ -47,7 +48,7 @@ class KafkaInputListenerTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestProcessor: RequestProcessor,
|
@Mock requestProcessor: RequestProcessor
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
this.objectMapper = ObjectMapper()
|
this.objectMapper = ObjectMapper()
|
||||||
@ -62,17 +63,9 @@ class KafkaInputListenerTest {
|
|||||||
.withConsent(Consent.builder().withStatus(Consent.Status.ACTIVE).build())
|
.withConsent(Consent.builder().withStatus(Consent.Status.ACTIVE).build())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(ConsumerRecord("testtopic", 0, 0, "", this.objectMapper.writeValueAsString(mtbFile)))
|
||||||
ConsumerRecord(
|
|
||||||
"testtopic",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"",
|
|
||||||
this.objectMapper.writeValueAsString(mtbFile)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -82,20 +75,9 @@ class KafkaInputListenerTest {
|
|||||||
.withConsent(Consent.builder().withStatus(Consent.Status.REJECTED).build())
|
.withConsent(Consent.builder().withStatus(Consent.Status.REJECTED).build())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(ConsumerRecord("testtopic", 0, 0, "", this.objectMapper.writeValueAsString(mtbFile)))
|
||||||
ConsumerRecord(
|
|
||||||
"testtopic",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"",
|
|
||||||
this.objectMapper.writeValueAsString(mtbFile)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(),
|
|
||||||
eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -107,22 +89,10 @@ class KafkaInputListenerTest {
|
|||||||
|
|
||||||
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(
|
||||||
ConsumerRecord(
|
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
|
||||||
"testtopic",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
-1L,
|
|
||||||
TimestampType.NO_TIMESTAMP_TYPE,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
"",
|
|
||||||
this.objectMapper.writeValueAsString(mtbFile),
|
|
||||||
headers,
|
|
||||||
Optional.empty()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>(), anyValueClass())
|
verify(requestProcessor, times(1)).processMtbFile(any(), anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -134,54 +104,9 @@ class KafkaInputListenerTest {
|
|||||||
|
|
||||||
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(
|
||||||
ConsumerRecord(
|
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
|
||||||
"testtopic",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
-1L,
|
|
||||||
TimestampType.NO_TIMESTAMP_TYPE,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
"",
|
|
||||||
this.objectMapper.writeValueAsString(mtbFile),
|
|
||||||
headers,
|
|
||||||
Optional.empty()
|
|
||||||
)
|
)
|
||||||
)
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass())
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass(), eq(
|
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotProcessDnpmV2Request() {
|
|
||||||
val mtbFile = MtbFile.builder()
|
|
||||||
.withPatient(Patient.builder().withId("DUMMY_12345678").build())
|
|
||||||
.withConsent(Consent.builder().withStatus(Consent.Status.REJECTED).build())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val headers = RecordHeaders(
|
|
||||||
listOf(
|
|
||||||
RecordHeader("requestId", UUID.randomUUID().toString().toByteArray()),
|
|
||||||
RecordHeader("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
kafkaInputListener.onMessage(
|
|
||||||
ConsumerRecord(
|
|
||||||
"testtopic",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
-1L,
|
|
||||||
TimestampType.NO_TIMESTAMP_TYPE,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
"",
|
|
||||||
this.objectMapper.writeValueAsString(mtbFile),
|
|
||||||
headers,
|
|
||||||
Optional.empty()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
verify(requestProcessor, times(0)).processDeletion(anyValueClass(), anyValueClass(), eq(
|
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -21,29 +21,18 @@ package dev.dnpm.etl.processor.input
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Consent.Status
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.consent.ConsentCheckFileBased
|
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource
|
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.times
|
import org.mockito.Mockito.times
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.anyValueClass
|
import org.mockito.kotlin.anyValueClass
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import org.springframework.core.io.ClassPathResource
|
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.context.TestPropertySource
|
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.delete
|
import org.springframework.test.web.servlet.delete
|
||||||
import org.springframework.test.web.servlet.post
|
import org.springframework.test.web.servlet.post
|
||||||
@ -61,22 +50,19 @@ class MtbFileRestControllerTest {
|
|||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestProcessor: RequestProcessor
|
@Mock requestProcessor: RequestProcessor
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
val controller = MtbFileRestController(requestProcessor)
|
||||||
ConsentCheckFileBased()
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequest() {
|
fun shouldProcessPostRequest() {
|
||||||
mockMvc.post("/mtbfile") {
|
mockMvc.post("/mtbfile") {
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -84,14 +70,13 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
fun shouldProcessPostRequestWithRejectedConsent() {
|
||||||
mockMvc.post("/mtbfile") {
|
mockMvc.post("/mtbfile") {
|
||||||
content =
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -99,10 +84,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -113,100 +95,10 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = ["app.consent.gics.enabled=true",
|
|
||||||
"app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"]
|
|
||||||
)
|
|
||||||
@Nested
|
|
||||||
inner class BwhcRequestsCheckConsentViaTtp {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
|
||||||
|
|
||||||
private lateinit var gicsConsentService: GicsConsentService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock requestProcessor: RequestProcessor,
|
|
||||||
@Mock gicsConsentService: GicsConsentService
|
|
||||||
) {
|
|
||||||
this.requestProcessor = requestProcessor
|
|
||||||
val controller = MtbFileRestController(requestProcessor, gicsConsentService)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
|
||||||
this.gicsConsentService = gicsConsentService
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(strings = ["ACTIVE", "REJECTED"])
|
|
||||||
fun shouldProcessPostRequest(status: String) {
|
|
||||||
|
|
||||||
whenever(gicsConsentService.getTtpConsentStatus(any())).thenReturn(TtpConsentStatus.CONSENTED)
|
|
||||||
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
content =
|
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status)))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@ValueSource(strings = ["ACTIVE", "REJECTED"])
|
|
||||||
fun shouldProcessPostRequestWithRejectedConsent(status: String) {
|
|
||||||
|
|
||||||
whenever(gicsConsentService.getTtpConsentStatus(any())).thenReturn(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED)
|
|
||||||
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
content =
|
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status)))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// consent status from ttp should override file consent value
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessDeleteRequest() {
|
|
||||||
|
|
||||||
mockMvc.delete("/mtbfile/TEST_12345678").andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
|
||||||
anyValueClass(),
|
|
||||||
org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
|
||||||
)
|
|
||||||
verify(gicsConsentService, times(0)).getTtpConsentStatus(any())
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class BwhcRequestsWithAlias {
|
inner class BwhcRequestsWithAlias {
|
||||||
|
|
||||||
@ -219,16 +111,14 @@ class MtbFileRestControllerTest {
|
|||||||
@Mock requestProcessor: RequestProcessor
|
@Mock requestProcessor: RequestProcessor
|
||||||
) {
|
) {
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
val controller = MtbFileRestController(requestProcessor)
|
||||||
ConsentCheckFileBased()
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequest() {
|
fun shouldProcessPostRequest() {
|
||||||
mockMvc.post("/mtb") {
|
mockMvc.post("/mtb") {
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -236,14 +126,13 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
fun shouldProcessPostRequestWithRejectedConsent() {
|
||||||
mockMvc.post("/mtb") {
|
mockMvc.post("/mtb") {
|
||||||
content =
|
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status {
|
status {
|
||||||
@ -251,11 +140,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(), org.mockito.kotlin.eq(
|
|
||||||
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -266,54 +151,12 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
anyValueClass(), org.mockito.kotlin.eq(
|
|
||||||
TtpConsentStatus.UNKNOWN_CHECK_FILE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class RequestsForDnpmDataModel21 {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock requestProcessor: RequestProcessor
|
|
||||||
) {
|
|
||||||
this.requestProcessor = requestProcessor
|
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
|
||||||
ConsentCheckFileBased()
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun bwhcMtbFileContent(consentStatus: Status) = MtbFile.builder()
|
fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("TEST_12345678")
|
.withId("TEST_12345678")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -21,40 +21,30 @@ package dev.dnpm.etl.processor.output
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
|
||||||
import org.apache.kafka.clients.producer.ProducerRecord
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.*
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.kafka.core.KafkaTemplate
|
||||||
import org.springframework.kafka.support.SendResult
|
import org.springframework.kafka.support.SendResult
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletableFuture.completedFuture
|
import java.util.concurrent.CompletableFuture.completedFuture
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class KafkaMtbFileSenderTest {
|
class KafkaMtbFileSenderTest {
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class BwhcV1Record {
|
|
||||||
|
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
|
||||||
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
||||||
@ -75,69 +65,67 @@ class KafkaMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
@MethodSource("requestWithResponseSource")
|
||||||
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
if (null != testData.exception) {
|
if (null != testData.exception) {
|
||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
val response = kafkaMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
val response = kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
@MethodSource("requestWithResponseSource")
|
||||||
fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) {
|
fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
if (null != testData.exception) {
|
if (null != testData.exception) {
|
||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
val response = kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldSendMtbFileRequestWithCorrectKeyAndHeaderAndBody() {
|
fun shouldSendMtbFileRequestWithCorrectKeyAndBody() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
||||||
|
|
||||||
val captor = argumentCaptor<ProducerRecord<String, String>>()
|
val captor = argumentCaptor<String>()
|
||||||
verify(kafkaTemplate, times(1)).send(captor.capture())
|
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
||||||
assertThat(captor.firstValue.key()).isNotNull
|
assertThat(captor.firstValue).isNotNull
|
||||||
assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}")
|
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
assertThat(captor.firstValue.headers().headers("contentType")).isNotNull
|
assertThat(captor.secondValue).isNotNull
|
||||||
assertThat(captor.firstValue.headers().headers("contentType")?.firstOrNull()?.value()).isEqualTo(MediaType.APPLICATION_JSON_VALUE.toByteArray())
|
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
|
||||||
assertThat(captor.firstValue.value()).isNotNull
|
|
||||||
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(bwhcV1kafkaRecordData(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
|
||||||
val captor = argumentCaptor<ProducerRecord<String, String>>()
|
val captor = argumentCaptor<String>()
|
||||||
verify(kafkaTemplate, times(1)).send(captor.capture())
|
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
||||||
assertThat(captor.firstValue.key()).isNotNull
|
assertThat(captor.firstValue).isNotNull
|
||||||
assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}")
|
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
assertThat(captor.firstValue.value()).isNotNull
|
assertThat(captor.secondValue).isNotNull
|
||||||
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(bwhcV1kafkaRecordData(TEST_REQUEST_ID, Consent.Status.REJECTED)))
|
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.REJECTED)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
@MethodSource("requestWithResponseSource")
|
||||||
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
@ -148,9 +136,9 @@ class KafkaMtbFileSenderTest {
|
|||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
|
||||||
|
|
||||||
val expectedCount = when (testData.exception) {
|
val expectedCount = when (testData.exception) {
|
||||||
// OK - No Retry
|
// OK - No Retry
|
||||||
@ -159,11 +147,11 @@ class KafkaMtbFileSenderTest {
|
|||||||
else -> times(3)
|
else -> times(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
@MethodSource("requestWithResponseSource")
|
||||||
fun shouldRetryOnDeleteKafkaSendError(testData: TestData) {
|
fun shouldRetryOnDeleteKafkaSendError(testData: TestData) {
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
@ -174,9 +162,9 @@ class KafkaMtbFileSenderTest {
|
|||||||
throw testData.exception
|
throw testData.exception
|
||||||
}
|
}
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
|
||||||
val expectedCount = when (testData.exception) {
|
val expectedCount = when (testData.exception) {
|
||||||
// OK - No Retry
|
// OK - No Retry
|
||||||
@ -185,98 +173,14 @@ class KafkaMtbFileSenderTest {
|
|||||||
else -> times(3)
|
else -> times(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class DnpmV2Record {
|
|
||||||
|
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
|
||||||
|
|
||||||
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
|
||||||
|
|
||||||
private lateinit var objectMapper: ObjectMapper
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock kafkaTemplate: KafkaTemplate<String, String>
|
|
||||||
) {
|
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.objectMapper = ObjectMapper()
|
|
||||||
this.kafkaTemplate = kafkaTemplate
|
|
||||||
|
|
||||||
this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
|
||||||
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
|
||||||
doAnswer {
|
|
||||||
if (null != testData.exception) {
|
|
||||||
throw testData.exception
|
|
||||||
}
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
|
||||||
|
|
||||||
val response = kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldSendMtbFileRequestWithCorrectKeyAndHeaderAndBody() {
|
|
||||||
doAnswer {
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
|
||||||
|
|
||||||
kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
|
||||||
|
|
||||||
val captor = argumentCaptor<ProducerRecord<String, String>>()
|
|
||||||
verify(kafkaTemplate, times(1)).send(captor.capture())
|
|
||||||
assertThat(captor.firstValue.key()).isNotNull
|
|
||||||
assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}")
|
|
||||||
assertThat(captor.firstValue.headers().headers("contentType")).isNotNull
|
|
||||||
assertThat(captor.firstValue.headers().headers("contentType")?.firstOrNull()?.value()).isEqualTo(CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
|
||||||
assertThat(captor.firstValue.value()).isNotNull
|
|
||||||
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(dnmpV2kafkaRecordData(TEST_REQUEST_ID)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
|
||||||
fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) {
|
|
||||||
val kafkaProperties = KafkaProperties("testtopic")
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
|
||||||
this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper)
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
if (null != testData.exception) {
|
|
||||||
throw testData.exception
|
|
||||||
}
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
|
||||||
|
|
||||||
kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
|
||||||
|
|
||||||
val expectedCount = when (testData.exception) {
|
|
||||||
// OK - No Retry
|
|
||||||
null -> times(1)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> times(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
fun bwhcV1MtbFile(consentStatus: Consent.Status): MtbFile {
|
fun mtbFile(consentStatus: Consent.Status): MtbFile {
|
||||||
return if (consentStatus == Consent.Status.ACTIVE) {
|
return if (consentStatus == Consent.Status.ACTIVE) {
|
||||||
MtbFile.builder()
|
MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -311,35 +215,8 @@ class KafkaMtbFileSenderTest {
|
|||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dnpmV2MtbFile(): Mtb {
|
fun kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): KafkaMtbFileSender.Data {
|
||||||
return Mtb().apply {
|
return KafkaMtbFileSender.Data(requestId, mtbFile(consentStatus))
|
||||||
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
this.birthDate = Date.from(Instant.now())
|
|
||||||
this.gender = GenderCoding().apply {
|
|
||||||
this.code = GenderCodingCode.MALE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.episodesOfCare = listOf(
|
|
||||||
MtbEpisodeOfCare().apply {
|
|
||||||
this.id = "1"
|
|
||||||
this.patient = Reference().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
}
|
|
||||||
this.period = PeriodDate().apply {
|
|
||||||
this.start = Date.from(Instant.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bwhcV1kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): MtbRequest {
|
|
||||||
return BwhcV1MtbFileRequest(requestId, bwhcV1MtbFile(consentStatus))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dnmpV2kafkaRecordData(requestId: RequestId): MtbRequest {
|
|
||||||
return DnpmV2MtbFileRequest(requestId, dnpmV2MtbFile())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
||||||
|
@ -30,16 +30,16 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
import org.springframework.http.HttpHeaders
|
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
import org.springframework.test.web.client.ExpectedCount
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -84,12 +84,11 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
this.mockRestServiceServer
|
this.mockRestServiceServer
|
||||||
.expect(method(HttpMethod.POST))
|
.expect(method(HttpMethod.POST))
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
||||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
|
||||||
.andRespond {
|
.andRespond {
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -119,7 +118,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -149,7 +148,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,6 @@ package dev.dnpm.etl.processor.output
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
@ -31,32 +29,25 @@ import dev.dnpm.etl.processor.config.AppConfiguration
|
|||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
import dev.dnpm.etl.processor.output.RestBwhcMtbFileSenderTest.Companion
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
import org.springframework.http.HttpHeaders
|
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.retry.backoff.NoBackOffPolicy
|
import org.springframework.retry.backoff.NoBackOffPolicy
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
import org.springframework.test.web.client.ExpectedCount
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class RestDipMtbFileSenderTest {
|
class RestDipMtbFileSenderTest {
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class BwhcV1ContentRequest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
@ -71,28 +62,41 @@ class RestDipMtbFileSenderTest {
|
|||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
|
|
||||||
this.restMtbFileSender =
|
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
@MethodSource("deleteRequestWithResponseSource")
|
||||||
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
||||||
this.mockRestServiceServer
|
this.mockRestServiceServer
|
||||||
.expect(method(HttpMethod.POST))
|
.expect(method(HttpMethod.DELETE))
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
||||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
|
||||||
.andRespond {
|
.andRespond {
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1mtbFile))
|
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
@MethodSource("mtbFileRequestWithResponseSource")
|
||||||
|
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
||||||
|
this.mockRestServiceServer
|
||||||
|
.expect(method(HttpMethod.POST))
|
||||||
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
||||||
|
.andRespond {
|
||||||
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mtbFileRequestWithResponseSource")
|
||||||
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
val restTemplate = RestTemplate()
|
val restTemplate = RestTemplate()
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
||||||
@ -119,89 +123,13 @@ class RestDipMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1mtbFile))
|
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class DnpmV2ContentRequest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
|
||||||
|
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
|
|
||||||
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForDnpmV2MtbFilePost(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.POST))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
|
||||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class DeleteRequest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
|
||||||
|
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.DELETE))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
@MethodSource("deleteRequestWithResponseSource")
|
||||||
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
val restTemplate = RestTemplate()
|
val restTemplate = RestTemplate()
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
||||||
@ -228,13 +156,11 @@ class RestDipMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
data class RequestWithResponse(
|
data class RequestWithResponse(
|
||||||
val httpStatus: HttpStatus,
|
val httpStatus: HttpStatus,
|
||||||
@ -245,7 +171,7 @@ class RestDipMtbFileSenderTest {
|
|||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
val bwhcV1mtbFile: MtbFile = MtbFile.builder()
|
val mtbFile: MtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("PID")
|
.withId("PID")
|
||||||
@ -269,29 +195,6 @@ class RestDipMtbFileSenderTest {
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun dnpmV2MtbFile(): Mtb {
|
|
||||||
return Mtb().apply {
|
|
||||||
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
this.birthDate = Date.from(Instant.now())
|
|
||||||
this.gender = GenderCoding().apply {
|
|
||||||
this.code = GenderCodingCode.MALE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.episodesOfCare = listOf(
|
|
||||||
MtbEpisodeOfCare().apply {
|
|
||||||
this.id = "1"
|
|
||||||
this.patient = Reference().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
}
|
|
||||||
this.period = PeriodDate().apply {
|
|
||||||
this.start = Date.from(Instant.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -21,11 +21,7 @@ package dev.dnpm.etl.processor.pseudonym
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
|
||||||
import dev.dnpm.etl.processor.config.JacksonConfig
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
@ -35,29 +31,20 @@ import org.mockito.kotlin.anyValueClass
|
|||||||
import org.mockito.kotlin.doAnswer
|
import org.mockito.kotlin.doAnswer
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.core.io.ClassPathResource
|
import org.springframework.core.io.ClassPathResource
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
const val FAKE_MTB_FILE_PATH = "fake_MTBFile.json"
|
||||||
|
const val CLEAN_PATIENT_ID = "5dad2f0b-49c6-47d8-a952-7b9e9e0f7549"
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class ExtensionsTest {
|
class ExtensionsTest {
|
||||||
fun getObjectMapper() : ObjectMapper {
|
|
||||||
return JacksonConfig().objectMapper()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class UsingBwhcDatamodel {
|
|
||||||
|
|
||||||
val FAKE_MTB_FILE_PATH = "fake_MTBFile.json"
|
|
||||||
val CLEAN_PATIENT_ID = "5dad2f0b-49c6-47d8-a952-7b9e9e0f7549"
|
|
||||||
|
|
||||||
|
|
||||||
private fun fakeMtbFile(): MtbFile {
|
private fun fakeMtbFile(): MtbFile {
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
||||||
return getObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
return ObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MtbFile.serialized(): String {
|
private fun MtbFile.serialized(): String {
|
||||||
return getObjectMapper().writeValueAsString(this)
|
return ObjectMapper().writeValueAsString(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -204,77 +191,8 @@ class ExtensionsTest {
|
|||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
mtbFile.anonymizeContentWith(pseudonymizeService)
|
||||||
|
|
||||||
|
|
||||||
assertThat(mtbFile.episode.id).isNotNull()
|
assertThat(mtbFile.episode.id).isNotNull()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class UsingDnpmV2Datamodel {
|
|
||||||
|
|
||||||
val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json"
|
|
||||||
val CLEAN_PATIENT_ID = "e14bf9b6-7982-4933-a648-cfdea6484f1c"
|
|
||||||
|
|
||||||
private fun fakeMtbFile(): Mtb {
|
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
|
||||||
return getObjectMapper().readValue(mtbFile, Mtb::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Mtb.serialized(): String {
|
|
||||||
return getObjectMapper().writeValueAsString(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
val mtbFile = fakeMtbFile()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
|
|
||||||
assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID")
|
|
||||||
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = Mtb().apply {
|
|
||||||
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
this.birthDate = Date.from(Instant.now())
|
|
||||||
this.gender = GenderCoding().apply {
|
|
||||||
this.code = GenderCodingCode.MALE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.episodesOfCare = listOf(
|
|
||||||
MtbEpisodeOfCare().apply {
|
|
||||||
this.id = "1"
|
|
||||||
this.patient = Reference().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
}
|
|
||||||
this.period = PeriodDate().apply {
|
|
||||||
this.start = Date.from(Instant.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
assertThat(mtbFile.episodesOfCare).hasSize(1)
|
|
||||||
assertThat(mtbFile.episodesOfCare.map { it.id }).isNotNull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -21,21 +21,14 @@ package dev.dnpm.etl.processor.services
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.Fingerprint
|
import dev.dnpm.etl.processor.*
|
||||||
import dev.dnpm.etl.processor.PatientId
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
|
||||||
import dev.dnpm.etl.processor.consent.TtpConsentStatus
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.output.BwhcV1MtbFileRequest
|
|
||||||
import dev.dnpm.etl.processor.output.DeleteRequest
|
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
import dev.dnpm.etl.processor.randomRequestId
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -60,7 +53,7 @@ class RequestProcessorTest {
|
|||||||
private lateinit var requestService: RequestService
|
private lateinit var requestService: RequestService
|
||||||
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
||||||
private lateinit var appConfigProperties: AppConfigProperties
|
private lateinit var appConfigProperties: AppConfigProperties
|
||||||
private lateinit var gicsConsentService : GicsConsentService
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -69,8 +62,7 @@ class RequestProcessorTest {
|
|||||||
@Mock transformationService: TransformationService,
|
@Mock transformationService: TransformationService,
|
||||||
@Mock sender: RestMtbFileSender,
|
@Mock sender: RestMtbFileSender,
|
||||||
@Mock requestService: RequestService,
|
@Mock requestService: RequestService,
|
||||||
@Mock applicationEventPublisher: ApplicationEventPublisher,
|
@Mock applicationEventPublisher: ApplicationEventPublisher
|
||||||
@Mock gicsConsentService: GicsConsentService
|
|
||||||
) {
|
) {
|
||||||
this.pseudonymizeService = pseudonymizeService
|
this.pseudonymizeService = pseudonymizeService
|
||||||
this.transformationService = transformationService
|
this.transformationService = transformationService
|
||||||
@ -78,7 +70,6 @@ class RequestProcessorTest {
|
|||||||
this.requestService = requestService
|
this.requestService = requestService
|
||||||
this.applicationEventPublisher = applicationEventPublisher
|
this.applicationEventPublisher = applicationEventPublisher
|
||||||
this.appConfigProperties = AppConfigProperties(null)
|
this.appConfigProperties = AppConfigProperties(null)
|
||||||
this.gicsConsentService = gicsConsentService
|
|
||||||
|
|
||||||
val objectMapper = ObjectMapper()
|
val objectMapper = ObjectMapper()
|
||||||
|
|
||||||
@ -89,8 +80,7 @@ class RequestProcessorTest {
|
|||||||
requestService,
|
requestService,
|
||||||
objectMapper,
|
objectMapper,
|
||||||
applicationEventPublisher,
|
applicationEventPublisher,
|
||||||
appConfigProperties,
|
appConfigProperties
|
||||||
gicsConsentService
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +109,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -178,7 +168,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -233,7 +223,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@ -241,7 +231,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -296,7 +286,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@ -304,7 +294,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -346,9 +336,9 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
val requestCaptor = argumentCaptor<Request>()
|
val requestCaptor = argumentCaptor<Request>()
|
||||||
verify(requestService, times(1)).save(requestCaptor.capture())
|
verify(requestService, times(1)).save(requestCaptor.capture())
|
||||||
@ -364,9 +354,9 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
val eventCaptor = argumentCaptor<ResponseEvent>()
|
val eventCaptor = argumentCaptor<ResponseEvent>()
|
||||||
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
||||||
@ -382,9 +372,9 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
val eventCaptor = argumentCaptor<ResponseEvent>()
|
val eventCaptor = argumentCaptor<ResponseEvent>()
|
||||||
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
||||||
@ -396,7 +386,7 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
||||||
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
val requestCaptor = argumentCaptor<Request>()
|
val requestCaptor = argumentCaptor<Request>()
|
||||||
verify(requestService, times(1)).save(requestCaptor.capture())
|
verify(requestService, times(1)).save(requestCaptor.capture())
|
||||||
@ -414,11 +404,11 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
|
@ -19,22 +19,14 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.Diagnosis
|
import de.ukw.ccc.bwhc.dto.Diagnosis
|
||||||
import de.ukw.ccc.bwhc.dto.Icd10
|
import de.ukw.ccc.bwhc.dto.Icd10
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.config.JacksonConfig
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.ModelProjectConsent
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.MvhMetadata
|
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource
|
|
||||||
import org.hl7.fhir.r4.model.CodeableConcept
|
|
||||||
import org.hl7.fhir.r4.model.Coding
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class TransformationServiceTest {
|
class TransformationServiceTest {
|
||||||
|
|
||||||
@ -43,7 +35,7 @@ class TransformationServiceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
this.service = TransformationService(
|
this.service = TransformationService(
|
||||||
JacksonConfig().objectMapper(), listOf(
|
ObjectMapper(), listOf(
|
||||||
Transformation.of("consent.status") from Consent.Status.ACTIVE to Consent.Status.REJECTED,
|
Transformation.of("consent.status") from Consent.Status.ACTIVE to Consent.Status.REJECTED,
|
||||||
Transformation.of("diagnoses[*].icd10.version") from "2013" to "2014",
|
Transformation.of("diagnoses[*].icd10.version") from "2013" to "2014",
|
||||||
)
|
)
|
||||||
@ -100,79 +92,4 @@ class TransformationServiceTest {
|
|||||||
assertThat(actual.consent.status).isEqualTo(Consent.Status.REJECTED)
|
assertThat(actual.consent.status).isEqualTo(Consent.Status.REJECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldTransformConsentValues() {
|
|
||||||
val mtbFile = MtbFile.builder().withDiagnoses(
|
|
||||||
listOf(
|
|
||||||
Diagnosis.builder().withId("1234").withIcd10(Icd10("F79.9").also {
|
|
||||||
it.version = "2013"
|
|
||||||
}).build(),
|
|
||||||
Diagnosis.builder().withId("5678").withIcd10(Icd10("F79.8").also {
|
|
||||||
it.version = "2019"
|
|
||||||
}).build()
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
val actual = this.service.transform(mtbFile)
|
|
||||||
|
|
||||||
assertThat(actual).isNotNull
|
|
||||||
assertThat(actual.diagnoses[0].icd10.code).isEqualTo("F79.9")
|
|
||||||
assertThat(actual.diagnoses[0].icd10.version).isEqualTo("2014")
|
|
||||||
assertThat(actual.diagnoses[1].icd10.code).isEqualTo("F79.8")
|
|
||||||
assertThat(actual.diagnoses[1].icd10.version).isEqualTo("2019")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldTransformConsent() {
|
|
||||||
val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build();
|
|
||||||
|
|
||||||
assertThat(mvhMetadata).isNotNull
|
|
||||||
mvhMetadata.modelProjectConsent =
|
|
||||||
ModelProjectConsent.builder().date(Date.from(Instant.now())).version("1").build()
|
|
||||||
val consent1 = org.hl7.fhir.r4.model.Consent()
|
|
||||||
consent1.id = "consent 1 id"
|
|
||||||
consent1.patient.reference = "Patient/1234-pat1"
|
|
||||||
|
|
||||||
consent1.provision.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny"))
|
|
||||||
consent1.provision.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
|
|
||||||
consent1.provision.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
|
|
||||||
|
|
||||||
|
|
||||||
val addProvision1 = consent1.provision.addProvision()
|
|
||||||
addProvision1.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("permit"))
|
|
||||||
addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
|
|
||||||
addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
|
|
||||||
addProvision1.code.addLast(
|
|
||||||
CodeableConcept(
|
|
||||||
Coding(
|
|
||||||
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"Teilnahme",
|
|
||||||
"Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val addProvision2 = consent1.provision.addProvision()
|
|
||||||
addProvision2.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny"))
|
|
||||||
addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
|
|
||||||
addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
|
|
||||||
addProvision2.code.addLast(
|
|
||||||
CodeableConcept(
|
|
||||||
Coding(
|
|
||||||
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"Rekontaktierung",
|
|
||||||
"Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
mvhMetadata.researchConsents = mutableListOf()
|
|
||||||
mvhMetadata.researchConsents.add(mapOf(consent1.id to consent1 as IBaseResource))
|
|
||||||
|
|
||||||
val mtbFile = Mtb.builder().metadata(mvhMetadata).build()
|
|
||||||
|
|
||||||
val transformed = service.transform(mtbFile)
|
|
||||||
assertThat(transformed.metadata.modelProjectConsent.date).isNotNull
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,333 +0,0 @@
|
|||||||
{
|
|
||||||
"resourceType": "Bundle",
|
|
||||||
"type": "collection",
|
|
||||||
"entry": [
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673204-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "24673204-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:58:27.178+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 999999"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673913-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "24673913-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:58:27.194+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 999999"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/24673da9-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "24673da9-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:58:27.211+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/2466d49b-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 999999"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/24670c77-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,333 +0,0 @@
|
|||||||
{
|
|
||||||
"resourceType": "Bundle",
|
|
||||||
"type": "collection",
|
|
||||||
"entry": [
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121a8368-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "121a8368-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:55:42.079+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 12345678"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "permit",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Teilnahme",
|
|
||||||
"display": "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121aad40-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "121aad40-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:55:42.096+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 12345678"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "permit",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Fallidentifizierung",
|
|
||||||
"display": "Fallidentifizierung zum fachlichen Austausch unter Behandelnden"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl": "http://127.0.0.1:8090/ttp-fhir/fhir/gics/Consent/121ac5f8-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"resource": {
|
|
||||||
"resourceType": "Consent",
|
|
||||||
"id": "121ac5f8-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"meta": {
|
|
||||||
"lastUpdated": "2025-06-24T11:55:42.110+02:00",
|
|
||||||
"profile": [
|
|
||||||
"http://fhir.de/ConsentManagement/StructureDefinition/Consent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "http://fhir.de/ConsentManagement/StructureDefinition/DomainReference",
|
|
||||||
"extension": [
|
|
||||||
{
|
|
||||||
"url": "domain",
|
|
||||||
"valueReference": {
|
|
||||||
"reference": "ResearchStudy/ef86d80e-50e0-11f0-a144-661e92ac9503"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "status",
|
|
||||||
"valueCoding": {
|
|
||||||
"system": "http://hl7.org/fhir/publication-status",
|
|
||||||
"code": "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": "active",
|
|
||||||
"scope": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
|
|
||||||
"code": "research"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://loinc.org",
|
|
||||||
"code": "59284-0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "http://fhir.de/ConsentManagement/CodeSystem/ResultType",
|
|
||||||
"code": "policy"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"patient": {
|
|
||||||
"reference": "Patient/12194791-50e1-11f0-a144-661e92ac9503",
|
|
||||||
"display": "Patienten-ID 12345678"
|
|
||||||
},
|
|
||||||
"dateTime": "2025-06-24T00:00:00+02:00",
|
|
||||||
"organization": [
|
|
||||||
{
|
|
||||||
"display": "GenomDE_MV"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sourceReference": {
|
|
||||||
"reference": "QuestionnaireResponse/1219ca42-50e1-11f0-a144-661e92ac9503"
|
|
||||||
},
|
|
||||||
"policyRule": {
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provision": {
|
|
||||||
"type": "deny",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"provision": [
|
|
||||||
{
|
|
||||||
"type": "permit",
|
|
||||||
"period": {
|
|
||||||
"start": "2025-06-24T00:00:00+02:00",
|
|
||||||
"end": "3000-01-01T00:00:00+01:00"
|
|
||||||
},
|
|
||||||
"code": [
|
|
||||||
{
|
|
||||||
"coding": [
|
|
||||||
{
|
|
||||||
"system": "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
|
|
||||||
"code": "Rekontaktierung",
|
|
||||||
"display": "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user