mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-07-01 06:02:54 +00:00
Compare commits
1 Commits
63-check-c
...
v0.9.6
Author | SHA1 | Date | |
---|---|---|---|
82361916d8 |
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@ -30,6 +30,6 @@ jobs:
|
|||||||
- name: Execute image build and push
|
- name: Execute image build and push
|
||||||
run: |
|
run: |
|
||||||
./gradlew bootBuildImage
|
./gradlew bootBuildImage
|
||||||
docker tag ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
docker tag ghcr.io/ccc-mf/etl-processor ghcr.io/ccc-mf/etl-processor:${{ github.ref_name }}
|
||||||
docker push ghcr.io/${{ github.repository }}
|
docker push ghcr.io/ccc-mf/etl-processor
|
||||||
docker push ghcr.io/${{ github.repository }}:${{ github.ref_name }}
|
docker push ghcr.io/ccc-mf/etl-processor:${{ github.ref_name }}
|
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*
|
|
||||||
|
123
README.md
123
README.md
@ -1,6 +1,6 @@
|
|||||||
# ETL-Processor for DNPM:DIP [](https://github.com/pcvolkmer/etl-processor/actions/workflows/test.yml)
|
# ETL-Processor for bwHC data [](https://github.com/CCC-MF/etl-processor/actions/workflows/test.yml)
|
||||||
|
|
||||||
Diese Anwendung versendet ein bwHC-MTB-File im bwHC-Datenmodell 1.0 an DNPM:DIP und pseudonymisiert die Patienten-ID.
|
Diese Anwendung versendet ein bwHC-MTB-File an das bwHC-Backend und pseudonymisiert die Patienten-ID.
|
||||||
|
|
||||||
## Einordnung innerhalb einer DNPM-ETL-Strecke
|
## Einordnung innerhalb einer DNPM-ETL-Strecke
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ Diese Anwendung erlaubt das Entgegennehmen von HTTP/REST-Anfragen aus dem Onkost
|
|||||||
Der Inhalt einer Anfrage, wenn ein bwHC-MTBFile, wird pseudonymisiert und auf Duplikate geprüft.
|
Der Inhalt einer Anfrage, wenn ein bwHC-MTBFile, wird pseudonymisiert und auf Duplikate geprüft.
|
||||||
Duplikate werden verworfen, Änderungen werden weitergeleitet.
|
Duplikate werden verworfen, Änderungen werden weitergeleitet.
|
||||||
|
|
||||||
Löschanfragen werden immer als Löschanfrage an DNPM:DIP weitergeleitet.
|
Löschanfragen werden immer als Löschanfrage an das bwHC-backend weitergeleitet.
|
||||||
|
|
||||||
Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand der Anwendung gewährt.
|
Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand der Anwendung gewährt.
|
||||||
|
|
||||||
@ -22,17 +22,7 @@ Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über de
|
|||||||
|
|
||||||
### Datenübermittlung über HTTP/REST
|
### Datenübermittlung über HTTP/REST
|
||||||
|
|
||||||
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an DNPM:DIP gesendet.
|
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an das bwHC-Backend gesendet.
|
||||||
|
|
||||||
Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an nachfolgende URLs gesendet werden:
|
|
||||||
|
|
||||||
| HTTP-Request | URL | Consent-Status im Datensatz | Bemerkung |
|
|
||||||
|--------------|-----------------------------------------|-----------------------------|---------------------------------------------------------------------------------|
|
|
||||||
| `POST` | `https://dnpm.example.com/mtb` | `ACTIVE` | Die Anwendung verarbeitet den eingehenden Datensatz |
|
|
||||||
| `POST` | `https://dnpm.example.com/mtb` | `REJECT` | Die Anwendung sendet einen Lösch-Request für die im Datensatz angegebene Pat-ID |
|
|
||||||
| `DELETE` | `https://dnpm.example.com/mtb/12345678` | - | Die Anwendung sendet einen Lösch-Request für Pat-ID `12345678` |
|
|
||||||
|
|
||||||
Anstelle des Pfads `/mtb` kann auch, wie in Version 0.9 und älter üblich, `/mtbfile` verwendet werden.
|
|
||||||
|
|
||||||
### Datenübermittlung mit Apache Kafka
|
### Datenübermittlung mit Apache Kafka
|
||||||
|
|
||||||
@ -43,21 +33,6 @@ Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc
|
|||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
### 🔥 Wichtige Änderungen in Version 0.10
|
|
||||||
|
|
||||||
Ab Version 0.10 wird [DNPM:DIP](https://github.com/dnpm-dip) unterstützt und als Standardendpunkt verwendet.
|
|
||||||
Soll noch das alte bwHC-Backend verwendet werden, so ist die Umgebungsvariable `APP_REST_IS_BWHC` auf `true` zu setzen.
|
|
||||||
|
|
||||||
### 🔥 Breaking Changes nach Version 0.10
|
|
||||||
|
|
||||||
In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen entfernt:
|
|
||||||
|
|
||||||
* `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC`
|
|
||||||
* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`
|
|
||||||
|
|
||||||
Der Pfad zum Versenden von MTB-Daten ist nun offiziell `/mtb`.
|
|
||||||
In Versionen **nach Version 0.10** wird die Unterstützung des Pfads `/mtbfile` entfernt.
|
|
||||||
|
|
||||||
### Pseudonymisierung der Patienten-ID
|
### Pseudonymisierung der Patienten-ID
|
||||||
|
|
||||||
Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) angegeben ist, wird diese verwendet.
|
Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) angegeben ist, wird diese verwendet.
|
||||||
@ -66,11 +41,13 @@ 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
|
||||||
Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den aktuellen Kontext nicht
|
werden.
|
||||||
vergleichbare IDs bereitzustellen.
|
* 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
|
||||||
|
vergleichbare IDs bereitzustellen.
|
||||||
|
|
||||||
#### Eingebaute Anonymisierung
|
#### Eingebaute Anonymisierung
|
||||||
|
|
||||||
@ -86,36 +63,10 @@ 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.
|
||||||
|
|
||||||
### Einwilligung gICS
|
Der Konfigurationsparameter `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION` sollte nicht mehr verwendet werden und wird in einer kommenden Version entfernt.
|
||||||
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.
|
Stattdessen sollte das Root Zertifikat wie unter [_Integration eines eigenen Root CA Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben eingebunden werden.
|
||||||
Vor der MTB-Übertragung kann der zum Sendezeitpunkt verfügbarer Einwilligungsstatus über Endpunkt *isConsented* abgefragt 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
|
||||||
|
|
||||||
@ -182,7 +133,7 @@ Sie bekommen dabei wieder die Standardrolle zugewiesen.
|
|||||||
#### Auswirkungen auf den dargestellten Inhalt
|
#### Auswirkungen auf den dargestellten Inhalt
|
||||||
|
|
||||||
Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die anonymisierte oder
|
Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die anonymisierte oder
|
||||||
pseudonymisierte Patienten-ID sowie den Qualitätsbericht von DNPM:DIP einsehen.
|
pseudonymisierte Patienten-ID sowie den Qualitätsbericht des bwHC-Backends einsehen.
|
||||||
|
|
||||||
Wurde kein Administrator-Account konfiguriert, sind diese Inhalte generell nicht verfügbar.
|
Wurde kein Administrator-Account konfiguriert, sind diese Inhalte generell nicht verfügbar.
|
||||||
|
|
||||||
@ -198,7 +149,7 @@ zur Nutzung des MTB-File-Endpunkts eine HTTP-Basic-Authentifizierung voraussetze
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
In diesem Fall kann der Endpunkt für das Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt konfiguriert werden:
|
In diesem Fall können den Endpunkt für das Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt konfigurieren:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
|
https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
|
||||||
@ -206,12 +157,10 @@ https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
|
|||||||
|
|
||||||
Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information abgelehnt.
|
Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information abgelehnt.
|
||||||
|
|
||||||
Alternativ kann eine Authentifizierung über Benutzername/Passwort oder OIDC erfolgen.
|
|
||||||
|
|
||||||
### Transformation von Werten
|
### Transformation von Werten
|
||||||
|
|
||||||
In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht,
|
In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht,
|
||||||
der von DNPM:DIP akzeptiert wird.
|
der vom bwHC-Backend akzeptiert wird.
|
||||||
|
|
||||||
Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der "Pfad" innerhalb des JSON-MTB-Files angegeben werden und
|
Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der "Pfad" innerhalb des JSON-MTB-Files angegeben werden und
|
||||||
welcher Wert wie ersetzt werden soll.
|
welcher Wert wie ersetzt werden soll.
|
||||||
@ -231,21 +180,18 @@ Werden sowohl REST als auch Kafka-Endpunkt konfiguriert, wird nur der REST-Endpu
|
|||||||
|
|
||||||
#### REST
|
#### REST
|
||||||
|
|
||||||
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an DNPM:DIP gesendet wird:
|
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an das bwHC-Backend gesendet wird:
|
||||||
|
|
||||||
* `APP_REST_URI`: URI der zu benutzenden API der Backend-Instanz. Zum Beispiel:
|
* `APP_REST_URI`: URI der zu benutzenden API der bwHC-Backend-Instanz. z.B.: `http://localhost:9000/bwhc/etl/api`
|
||||||
* `http://localhost:9000/bwhc/etl/api` für **bwHC Backend**
|
|
||||||
* `http://localhost:9000/api` für **dnpm:dip**
|
|
||||||
* `APP_REST_USERNAME`: Basic-Auth-Benutzername für den REST-Endpunkt
|
|
||||||
* `APP_REST_PASSWORD`: Basic-Auth-Passwort für den REST-Endpunkt
|
|
||||||
* `APP_REST_IS_BWHC`: `true` für **bwHC Backend**, weglassen oder `false` für **dnpm:dip**
|
|
||||||
|
|
||||||
#### Kafka-Topics
|
#### Kafka-Topics
|
||||||
|
|
||||||
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 in einer kommenden Version `APP_KAFKA_TOPIC`.
|
||||||
* `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 in einer kommenden Version `APP_KAFKA_RESPONSE_TOPIC`.
|
||||||
* `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
|
||||||
|
|
||||||
@ -253,7 +199,7 @@ Wird keine Rückantwort über Apache Kafka empfangen und es gibt keine weitere M
|
|||||||
|
|
||||||
Weitere Einstellungen können über die Parameter von Spring Kafka konfiguriert werden.
|
Weitere Einstellungen können über die Parameter von Spring Kafka konfiguriert werden.
|
||||||
|
|
||||||
Lässt sich keine Verbindung zu dem Backend aufbauen, wird eine Rückantwort mit Status-Code `900` erwartet, welchen es
|
Lässt sich keine Verbindung zu dem bwHC-Backend aufbauen, wird eine Rückantwort mit Status-Code `900` erwartet, welchen es
|
||||||
für HTTP nicht gibt.
|
für HTTP nicht gibt.
|
||||||
|
|
||||||
Wird die Umgebungsvariable `APP_KAFKA_INPUT_TOPIC` gesetzt, kann eine Nachricht auch über dieses Kafka-Topic an den ETL-Prozessor übermittelt werden.
|
Wird die Umgebungsvariable `APP_KAFKA_INPUT_TOPIC` gesetzt, kann eine Nachricht auch über dieses Kafka-Topic an den ETL-Prozessor übermittelt werden.
|
||||||
@ -290,7 +236,7 @@ kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-co
|
|||||||
Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger Konfiguration
|
Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger Konfiguration
|
||||||
der Kafka-Topics nach 10 Sekunden nur noch der jeweils letzte Eintrag für den entsprechenden Key zur Verfügung.
|
der Kafka-Topics nach 10 Sekunden nur noch der jeweils letzte Eintrag für den entsprechenden Key zur Verfügung.
|
||||||
|
|
||||||
Da der Key sowohl für die Records in Richtung DNPM:DIP, als auch für die Rückantwort identisch aufgebaut ist, lassen sich so
|
Da der Key sowohl für die Records in Richtung bwHC-Backend für die Rückantwort identisch aufgebaut ist, lassen sich so
|
||||||
auch im Falle eines Consent-Widerspruchs die enthaltenen Daten als auch die Offenlegung durch Verifikationsdaten in der
|
auch im Falle eines Consent-Widerspruchs die enthaltenen Daten als auch die Offenlegung durch Verifikationsdaten in der
|
||||||
Antwort effektiv verhindern, da diese nach 10 Sekunden gelöscht werden.
|
Antwort effektiv verhindern, da diese nach 10 Sekunden gelöscht werden.
|
||||||
|
|
||||||
@ -299,30 +245,9 @@ ein Consent-Widerspruch erfolgte.
|
|||||||
|
|
||||||
Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen verwenden möchten.
|
Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen verwenden möchten.
|
||||||
|
|
||||||
### Antworten und Statusauswertung
|
|
||||||
|
|
||||||
Anfragen an das bwHC-Backend aus Versionen bis 0.9.x wurden wie folgt behandelt:
|
|
||||||
|
|
||||||
| HTTP-Response | Status |
|
|
||||||
|----------------|-----------|
|
|
||||||
| `HTTP 200` | `SUCCESS` |
|
|
||||||
| `HTTP 201` | `WARNING` |
|
|
||||||
| `HTTP 400-...` | `ERROR` |
|
|
||||||
|
|
||||||
Dies konnte dazu führen, dass zwar mit einem `HTTP 201` geantwortet wurde, aber dennoch in der Issue-Liste die
|
|
||||||
Severity `error` aufgetaucht ist.
|
|
||||||
|
|
||||||
Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste Severity-Stufe als Ergebnis verwendet.
|
|
||||||
|
|
||||||
| Höchste Severity | Status |
|
|
||||||
|------------------|-----------|
|
|
||||||
| `info` | `SUCCESS` |
|
|
||||||
| `warning` | `WARNING` |
|
|
||||||
| `error`, `fatal` | `ERROR` |
|
|
||||||
|
|
||||||
## Docker-Images
|
## Docker-Images
|
||||||
|
|
||||||
Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor
|
Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/CCC-MF/etl-processor/pkgs/container/etl-processor
|
||||||
|
|
||||||
### Images lokal bauen
|
### Images lokal bauen
|
||||||
|
|
||||||
@ -435,5 +360,3 @@ Die Datei `application-dev.yml` enthält hierzu die Konfiguration für das Profi
|
|||||||
|
|
||||||
Beim Ausführen der Integrationstests wird eine Testdatenbank in einem Docker-Container gestartet.
|
Beim Ausführen der Integrationstests wird eine Testdatenbank in einem Docker-Container gestartet.
|
||||||
Siehe hier auch die Klasse `AbstractTestcontainerTest` unter `src/integrationTest`.
|
Siehe hier auch die Klasse `AbstractTestcontainerTest` unter `src/integrationTest`.
|
||||||
|
|
||||||
Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar.
|
|
||||||
|
@ -1,36 +1,30 @@
|
|||||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
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.2.7"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.5"
|
||||||
kotlin("jvm") version "1.9.25"
|
kotlin("jvm") version "1.9.24"
|
||||||
kotlin("plugin.spring") version "1.9.25"
|
kotlin("plugin.spring") version "1.9.24"
|
||||||
jacoco
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.dnpm"
|
group = "de.ukw.ccc"
|
||||||
version = "0.11.0-SNAPSHOT"
|
version = "0.9.6"
|
||||||
|
|
||||||
var versions = mapOf(
|
var versions = mapOf(
|
||||||
"bwhc-dto-java" to "0.4.0",
|
"bwhc-dto-java" to "0.3.0",
|
||||||
"mtb-dto" to "0.1.0-SNAPSHOT",
|
"hapi-fhir" to "6.10.5",
|
||||||
"hapi-fhir" to "7.6.0",
|
"httpclient5" to "5.2.3",
|
||||||
"mockito-kotlin" to "5.4.0",
|
"mockito-kotlin" to "5.3.1",
|
||||||
"archunit" to "1.3.0",
|
|
||||||
// Webjars
|
// Webjars
|
||||||
"webjars-locator" to "0.52",
|
|
||||||
"echarts" to "5.4.3",
|
"echarts" to "5.4.3",
|
||||||
"htmx.org" to "1.9.12"
|
"htmx.org" to "1.9.12"
|
||||||
)
|
)
|
||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@ -49,18 +43,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")
|
||||||
}
|
}
|
||||||
@ -77,46 +62,37 @@ dependencies {
|
|||||||
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
|
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
implementation("org.springframework.kafka:spring-kafka")
|
implementation("org.springframework.kafka:spring-kafka")
|
||||||
implementation("org.flywaydb:flyway-database-postgresql")
|
|
||||||
implementation("org.flywaydb:flyway-mysql")
|
implementation("org.flywaydb:flyway-mysql")
|
||||||
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:${versions["httpclient5"]}")
|
||||||
implementation("com.jayway.jsonpath:json-path")
|
implementation("com.jayway.jsonpath:json-path")
|
||||||
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
|
implementation("org.webjars:webjars-locator:0.52")
|
||||||
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
|
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
|
||||||
implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}")
|
implementation("org.webjars.npm:htmx.org:${versions["htmx.org"]}")
|
||||||
|
|
||||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
|
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
|
||||||
|
|
||||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
|
|
||||||
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
|
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
|
||||||
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testImplementation("org.springframework.security:spring-security-test")
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
testImplementation("io.projectreactor:reactor-test")
|
testImplementation("io.projectreactor:reactor-test")
|
||||||
testImplementation("org.mockito.kotlin:mockito-kotlin:${versions["mockito-kotlin"]}")
|
testImplementation("org.mockito.kotlin:mockito-kotlin:${versions["mockito-kotlin"]}")
|
||||||
|
|
||||||
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"]}")
|
// Override dependency version from org.testcontainers:junit-jupiter - CVE-2024-26308, CVE-2024-25710
|
||||||
integrationTestImplementation("org.htmlunit:htmlunit")
|
integrationTestImplementation("org.apache.commons:commons-compress:1.26.1")
|
||||||
integrationTestImplementation("org.springframework:spring-webflux")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
compilerOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs.add("-Xjsr305=strict")
|
freeCompilerArgs += "-Xjsr305=strict"
|
||||||
jvmTarget.set(JvmTarget.JVM_21)
|
jvmTarget = "21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,9 +103,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
|
||||||
@ -137,24 +112,8 @@ tasks.register<Test>("integrationTest") {
|
|||||||
shouldRunAfter("test")
|
shouldRunAfter("test")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("allTests") {
|
|
||||||
description = "Run all tests"
|
|
||||||
group = JavaBasePlugin.VERIFICATION_GROUP
|
|
||||||
dependsOn(tasks.withType<Test>())
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.jacocoTestReport {
|
|
||||||
dependsOn("allTests")
|
|
||||||
|
|
||||||
executionData(fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec"))
|
|
||||||
|
|
||||||
reports {
|
|
||||||
xml.required = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named<BootBuildImage>("bootBuildImage") {
|
tasks.named<BootBuildImage>("bootBuildImage") {
|
||||||
imageName.set("ghcr.io/pcvolkmer/etl-processor")
|
imageName.set("ghcr.io/ccc-mf/etl-processor")
|
||||||
|
|
||||||
// Binding for CA Certs
|
// Binding for CA Certs
|
||||||
bindings.set(listOf(
|
bindings.set(listOf(
|
||||||
@ -164,7 +123,7 @@ tasks.named<BootBuildImage>("bootBuildImage") {
|
|||||||
environment.set(environment.get() + mapOf(
|
environment.set(environment.get() + mapOf(
|
||||||
// Enable this line to embed CA Certs into image on build time
|
// Enable this line to embed CA Certs into image on build time
|
||||||
//"BP_EMBED_CERTS" to "true",
|
//"BP_EMBED_CERTS" to "true",
|
||||||
"BP_OCI_SOURCE" to "https://github.com/pcvolkmer/etl-processor",
|
"BP_OCI_SOURCE" to "https://github.com/CCC-MF/etl-processor",
|
||||||
"BP_OCI_LICENSES" to "AGPLv3",
|
"BP_OCI_LICENSES" to "AGPLv3",
|
||||||
"BP_OCI_DESCRIPTION" to "ETL Processor for bwHC MTB files"
|
"BP_OCI_DESCRIPTION" to "ETL Processor for bwHC MTB files"
|
||||||
))
|
))
|
||||||
|
@ -18,9 +18,6 @@ services:
|
|||||||
APP_KAFKA_GROUP_ID: ${DNPM_KAFKA_GROUP_ID}
|
APP_KAFKA_GROUP_ID: ${DNPM_KAFKA_GROUP_ID}
|
||||||
APP_KAFKA_RESPONSE_TOPIC: ${DNPM_KAFKA_RESPONSE_TOPIC}
|
APP_KAFKA_RESPONSE_TOPIC: ${DNPM_KAFKA_RESPONSE_TOPIC}
|
||||||
APP_REST_URI: ${DNPM_BWHC_REST_URI}
|
APP_REST_URI: ${DNPM_BWHC_REST_URI}
|
||||||
APP_REST_USERNAME: ${DNPM_BWHC_REST_USERNAME}
|
|
||||||
APP_REST_PASSWORD: ${DNPM_BWHC_REST_PASSWORD}
|
|
||||||
APP_REST_IS_BWHC: ${DNPM_BWHC_REST_IS_BWHC}
|
|
||||||
APP_SECURITY_ADMIN_USER: ${DNPM_ADMIN_USER}
|
APP_SECURITY_ADMIN_USER: ${DNPM_ADMIN_USER}
|
||||||
APP_SECURITY_ADMIN_PASSWORD: ${DNPM_ADMIN_PASSWORD}
|
APP_SECURITY_ADMIN_PASSWORD: ${DNPM_ADMIN_PASSWORD}
|
||||||
SPRING_DATASOURCE_URL: ${DNPM_DATASOURCE_URL}
|
SPRING_DATASOURCE_URL: ${DNPM_DATASOURCE_URL}
|
||||||
|
@ -28,9 +28,6 @@ DNPM_DATASOURCE_URL=jdbc:mariadb://dnpm-monitor-db:3306/$DNPM_MARIADB_DB
|
|||||||
## TARGET SYSTEMS CONFIG
|
## TARGET SYSTEMS CONFIG
|
||||||
# in case of direct access to bwhc enter endpoint url here
|
# in case of direct access to bwhc enter endpoint url here
|
||||||
DNPM_BWHC_REST_URI=
|
DNPM_BWHC_REST_URI=
|
||||||
DNPM_BWHC_REST_USERNAME=
|
|
||||||
DNPM_BWHC_REST_PASSWORD=
|
|
||||||
DNPM_BWHC_REST_IS_BWHC=false
|
|
||||||
|
|
||||||
# produce mtb files to this topic - values 'false' disabling kafka processing
|
# produce mtb files to this topic - values 'false' disabling kafka processing
|
||||||
DNPM_KAFKA_TOPIC=false
|
DNPM_KAFKA_TOPIC=false
|
||||||
|
@ -16,15 +16,9 @@ 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
|
|
||||||
akhq:
|
akhq:
|
||||||
image: tchiotludo/akhq:0.25.0
|
image: tchiotludo/akhq:0.21.0
|
||||||
environment:
|
environment:
|
||||||
AKHQ_CONFIGURATION: |
|
AKHQ_CONFIGURATION: |
|
||||||
akhq:
|
akhq:
|
||||||
@ -38,8 +32,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8084:8080"
|
- "8084:8080"
|
||||||
|
|
||||||
|
|
||||||
## For use with MariaDB
|
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:10
|
image: mariadb:10
|
||||||
ports:
|
ports:
|
||||||
@ -50,7 +42,6 @@ services:
|
|||||||
MARIADB_PASSWORD: dev
|
MARIADB_PASSWORD: dev
|
||||||
MARIADB_ROOT_PASSWORD: dev
|
MARIADB_ROOT_PASSWORD: dev
|
||||||
|
|
||||||
## For use with Postgres
|
|
||||||
# postgres:
|
# postgres:
|
||||||
# image: postgres:alpine
|
# image: postgres:alpine
|
||||||
# ports:
|
# ports:
|
||||||
@ -58,4 +49,4 @@ services:
|
|||||||
# environment:
|
# environment:
|
||||||
# POSTGRES_DB: dev
|
# POSTGRES_DB: dev
|
||||||
# POSTGRES_USER: dev
|
# POSTGRES_USER: dev
|
||||||
# POSTGRES_PASSWORD: dev
|
# POSTGRES_PASSWORD: dev
|
@ -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
|
||||||
|
BIN
docs/etl.png
BIN
docs/etl.png
Binary file not shown.
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 120 KiB |
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor
|
|
||||||
|
|
||||||
import com.tngtech.archunit.core.domain.JavaClasses
|
|
||||||
import com.tngtech.archunit.core.importer.ClassFileImporter
|
|
||||||
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
|
|
||||||
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.springframework.data.repository.Repository
|
|
||||||
|
|
||||||
class EtlProcessorArchTest {
|
|
||||||
|
|
||||||
private lateinit var noTestClasses: JavaClasses
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setUp() {
|
|
||||||
this.noTestClasses = ClassFileImporter()
|
|
||||||
.withImportOption { !(it.contains("/test/") || it.contains("/integrationTest/")) }
|
|
||||||
.importPackages("dev.dnpm.etl.processor")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun noClassesInInputPackageShouldDependOnMonitoringPackage() {
|
|
||||||
val rule = noClasses()
|
|
||||||
.that()
|
|
||||||
.resideInAPackage("..input")
|
|
||||||
.should().dependOnClassesThat()
|
|
||||||
.resideInAnyPackage("..monitoring")
|
|
||||||
|
|
||||||
rule.check(noTestClasses)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun noClassesInInputPackageShouldDependOnRepositories() {
|
|
||||||
val rule = noClasses()
|
|
||||||
.that()
|
|
||||||
.resideInAPackage("..input")
|
|
||||||
.should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
|
|
||||||
|
|
||||||
rule.check(noTestClasses)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun noClassesInOutputPackageShouldDependOnRepositories() {
|
|
||||||
val rule = noClasses()
|
|
||||||
.that()
|
|
||||||
.resideInAPackage("..output")
|
|
||||||
.should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
|
|
||||||
|
|
||||||
rule.check(noTestClasses)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun noClassesInWebPackageShouldDependOnRepositories() {
|
|
||||||
val rule = noClasses()
|
|
||||||
.that()
|
|
||||||
.resideInAPackage("..web")
|
|
||||||
.should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
|
|
||||||
|
|
||||||
rule.check(noTestClasses)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun repositoryClassNamesShouldEndWithRepository() {
|
|
||||||
val rule = classes()
|
|
||||||
.that()
|
|
||||||
.areInterfaces().and().areAssignableTo(Repository::class.java)
|
|
||||||
.should().haveSimpleNameEndingWith("Repository")
|
|
||||||
|
|
||||||
rule.check(noTestClasses)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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.security.TokenRepository
|
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
|
import dev.dnpm.etl.processor.services.TokenRepository
|
||||||
|
import dev.dnpm.etl.processor.services.TokenService
|
||||||
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
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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
|
|
||||||
|
|
||||||
import org.mockito.ArgumentMatchers
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
inline fun <reified T> anyValueClass(): T {
|
|
||||||
val unboxedClass = T::class.java.declaredFields.first().type
|
|
||||||
return ArgumentMatchers.any(unboxedClass as Class<T>)
|
|
||||||
?: T::class.java.getDeclaredMethod("box-impl", unboxedClass)
|
|
||||||
.invoke(null, null) as T
|
|
||||||
}
|
|
@ -21,29 +21,26 @@ 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 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.UserRoleRepository
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
|
import dev.dnpm.etl.processor.services.TokenRepository
|
||||||
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.mockito.ArgumentMatchers.anyString
|
||||||
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.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 +51,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,20 +88,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldGrantPermissionToSendMtbFileToAdminUser() {
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
with(user("onkostarserver").roles("ADMIN"))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
content = ObjectMapper().writeValueAsString(mtbFile)
|
|
||||||
}.andExpect {
|
|
||||||
status { isAccepted() }
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -119,20 +101,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, never()).processMtbFile(any())
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldDenyPermissionToSendMtbFileForUser() {
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
with(user("fakeuser").roles("USER"))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
content = ObjectMapper().writeValueAsString(mtbFile)
|
|
||||||
}.andExpect {
|
|
||||||
status { isForbidden() }
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -143,7 +112,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE))
|
verify(requestProcessor, times(1)).processDeletion(anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -154,47 +123,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processDeletion(anyValueClass(), any())
|
verify(requestProcessor, never()).processDeletion(anyString())
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::class])
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
|
||||||
"app.security.admin-user=admin",
|
|
||||||
"app.security.admin-password={noop}very-secret",
|
|
||||||
"app.security.enable-tokens=true",
|
|
||||||
"app.security.enable-oidc=true",
|
|
||||||
"app.consent.gics.enabled=false"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class WithOidcEnabled {
|
|
||||||
@Test
|
|
||||||
fun testShouldGrantPermissionToSendMtbFileToAdminUser() {
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
with(user("onkostarserver").roles("ADMIN"))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
content = ObjectMapper().writeValueAsString(mtbFile)
|
|
||||||
}.andExpect {
|
|
||||||
status { isAccepted() }
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldGrantPermissionToSendMtbFileToUser() {
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
with(user("onkostarserver").roles("USER"))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
content = ObjectMapper().writeValueAsString(mtbFile)
|
|
||||||
}.andExpect {
|
|
||||||
status { isAccepted() }
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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.monitoring
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.*
|
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
|
|
||||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
|
||||||
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.transaction.annotation.Transactional
|
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@Testcontainers
|
|
||||||
@ExtendWith(SpringExtension::class)
|
|
||||||
@DataJdbcTest
|
|
||||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
|
||||||
@Transactional
|
|
||||||
@MockitoBean(types = [MtbFileSender::class])
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=buildin",
|
|
||||||
"app.rest.uri=http://example.com"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
class RequestRepositoryTest : AbstractTestcontainerTest() {
|
|
||||||
|
|
||||||
private lateinit var requestRepository: RequestRepository
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setUp(
|
|
||||||
@Autowired requestRepository: RequestRepository
|
|
||||||
) {
|
|
||||||
this.requestRepository = requestRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldSaveRequest() {
|
|
||||||
val request = Request(
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("TEST_12345678901"),
|
|
||||||
PatientId("P1"),
|
|
||||||
Fingerprint("0123456789abcdef1"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.WARNING,
|
|
||||||
Instant.parse("2023-07-07T00:00:00Z")
|
|
||||||
)
|
|
||||||
|
|
||||||
requestRepository.save(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,147 +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.pseudonym
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.AppFhirConfig
|
|
||||||
import dev.dnpm.etl.processor.config.GPasConfigProperties
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.assertThrows
|
|
||||||
import org.springframework.http.HttpHeaders
|
|
||||||
import org.springframework.http.HttpMethod
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
|
||||||
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.withException
|
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
|
||||||
import org.springframework.web.client.RestTemplate
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class GpasPseudonymGeneratorTest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
|
||||||
private lateinit var generator: GpasPseudonymGenerator
|
|
||||||
private lateinit var restTemplate: RestTemplate
|
|
||||||
private var appFhirConfig: AppFhirConfig = AppFhirConfig()
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
val gPasConfigProperties = GPasConfigProperties(
|
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
|
||||||
"test",
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
this.restTemplate = RestTemplate()
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
this.generator =
|
|
||||||
GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate, appFhirConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldReturnExpectedPseudonym() {
|
|
||||||
this.mockRestServiceServer.expect {
|
|
||||||
method(HttpMethod.POST)
|
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
|
||||||
}.andRespond {
|
|
||||||
withStatus(HttpStatus.OK).body(
|
|
||||||
getDummyResponseBody(
|
|
||||||
"1234",
|
|
||||||
"test",
|
|
||||||
"test1234ABCDEF567890"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(this.generator.generate("ID1234")).isEqualTo("test1234ABCDEF567890")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldThrowExceptionIfGpasNotAvailable() {
|
|
||||||
this.mockRestServiceServer.expect {
|
|
||||||
method(HttpMethod.POST)
|
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
|
||||||
}.andRespond {
|
|
||||||
withException(IOException("Simulated IO error")).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
|
|
||||||
this.mockRestServiceServer.expect {
|
|
||||||
method(HttpMethod.POST)
|
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
|
||||||
}.andRespond {
|
|
||||||
withStatus(HttpStatus.FOUND)
|
|
||||||
.header(
|
|
||||||
HttpHeaders.LOCATION,
|
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate"
|
|
||||||
)
|
|
||||||
.createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun getDummyResponseBody(original: String, target: String, pseudonym: String) = """{
|
|
||||||
"resourceType": "Parameters",
|
|
||||||
"parameter": [
|
|
||||||
{
|
|
||||||
"name": "pseudonym",
|
|
||||||
"part": [
|
|
||||||
{
|
|
||||||
"name": "original",
|
|
||||||
"valueIdentifier": {
|
|
||||||
"system": "https://ths-greifswald.de/gpas",
|
|
||||||
"value": "$original"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "target",
|
|
||||||
"valueIdentifier": {
|
|
||||||
"system": "https://ths-greifswald.de/gpas",
|
|
||||||
"value": "$target"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pseudonym",
|
|
||||||
"valueIdentifier": {
|
|
||||||
"system": "https://ths-greifswald.de/gpas",
|
|
||||||
"value": "$pseudonym"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}""".trimIndent()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.AbstractTestcontainerTest
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
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
|
||||||
@ -31,18 +31,19 @@ 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
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@Testcontainers
|
@Testcontainers
|
||||||
@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",
|
||||||
@ -65,7 +66,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldResultInEmptyRequestList() {
|
fun shouldResultInEmptyRequestList() {
|
||||||
val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM)
|
val actual = requestService.allRequestsByPatientPseudonym("TEST_12345678901")
|
||||||
|
|
||||||
assertThat(actual).isEmpty()
|
assertThat(actual).isEmpty()
|
||||||
}
|
}
|
||||||
@ -75,33 +76,33 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
|
|||||||
this.requestRepository.saveAll(
|
this.requestRepository.saveAll(
|
||||||
listOf(
|
listOf(
|
||||||
Request(
|
Request(
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-07-07T02:00:00Z")
|
processedAt = Instant.parse("2023-07-07T02:00:00Z")
|
||||||
),
|
),
|
||||||
// Should be ignored - wrong patient ID -->
|
// Should be ignored - wrong patient ID -->
|
||||||
Request(
|
Request(
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678902"),
|
patientId = "TEST_12345678902",
|
||||||
PatientId("P2"),
|
pid = "P2",
|
||||||
Fingerprint("0123456789abcdef2"),
|
fingerprint = "0123456789abcdef2",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.WARNING,
|
status = RequestStatus.WARNING,
|
||||||
Instant.parse("2023-08-08T00:00:00Z")
|
processedAt = Instant.parse("2023-08-08T00:00:00Z")
|
||||||
),
|
),
|
||||||
// <--
|
// <--
|
||||||
Request(
|
Request(
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P2"),
|
pid = "P2",
|
||||||
Fingerprint("0123456789abcdee1"),
|
fingerprint = "0123456789abcdee1",
|
||||||
RequestType.DELETE,
|
type = RequestType.DELETE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
processedAt = Instant.parse("2023-08-08T02:00:00Z")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -111,18 +112,18 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
|
|||||||
fun shouldResultInSortedRequestList() {
|
fun shouldResultInSortedRequestList() {
|
||||||
setupTestData()
|
setupTestData()
|
||||||
|
|
||||||
val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM)
|
val actual = requestService.allRequestsByPatientPseudonym("TEST_12345678901")
|
||||||
|
|
||||||
assertThat(actual).hasSize(2)
|
assertThat(actual).hasSize(2)
|
||||||
assertThat(actual[0].fingerprint).isEqualTo(Fingerprint("0123456789abcdee1"))
|
assertThat(actual[0].fingerprint).isEqualTo("0123456789abcdee1")
|
||||||
assertThat(actual[1].fingerprint).isEqualTo(Fingerprint("0123456789abcdef1"))
|
assertThat(actual[1].fingerprint).isEqualTo("0123456789abcdef1")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldReturnDeleteRequestAsLastRequest() {
|
fun shouldReturnDeleteRequestAsLastRequest() {
|
||||||
setupTestData()
|
setupTestData()
|
||||||
|
|
||||||
val actual = requestService.isLastRequestWithKnownStatusDeletion(TEST_PATIENT_PSEUDONYM)
|
val actual = requestService.isLastRequestWithKnownStatusDeletion("TEST_12345678901")
|
||||||
|
|
||||||
assertThat(actual).isTrue()
|
assertThat(actual).isTrue()
|
||||||
}
|
}
|
||||||
@ -131,14 +132,10 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
|
|||||||
fun shouldReturnLastMtbFileRequest() {
|
fun shouldReturnLastMtbFileRequest() {
|
||||||
setupTestData()
|
setupTestData()
|
||||||
|
|
||||||
val actual = requestService.lastMtbFileRequestForPatientPseudonym(TEST_PATIENT_PSEUDONYM)
|
val actual = requestService.lastMtbFileRequestForPatientPseudonym("TEST_12345678901")
|
||||||
|
|
||||||
assertThat(actual).isNotNull
|
assertThat(actual).isNotNull
|
||||||
assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef1"))
|
assertThat(actual?.fingerprint).isEqualTo("0123456789abcdef1")
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("TEST_12345678901")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -21,50 +21,30 @@ package dev.dnpm.etl.processor.web
|
|||||||
|
|
||||||
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.ConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.monitoring.GIcsConnectionCheckService
|
|
||||||
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.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.services.TokenRepository
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
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.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.argumentCaptor
|
|
||||||
import org.mockito.kotlin.times
|
|
||||||
import org.mockito.kotlin.verify
|
|
||||||
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.security.test.context.support.WithMockUser
|
|
||||||
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.reactive.server.WebTestClient
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.*
|
import org.springframework.test.web.servlet.get
|
||||||
import org.springframework.test.web.servlet.client.MockMvcWebTestClient
|
|
||||||
import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
|
|
||||||
import org.springframework.web.context.WebApplicationContext
|
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
import reactor.test.StepVerifier
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
abstract class MockSink : Sinks.Many<Boolean>
|
abstract class MockSink : Sinks.Many<Boolean>
|
||||||
|
|
||||||
@ -79,51 +59,44 @@ abstract class MockSink : Sinks.Many<Boolean>
|
|||||||
)
|
)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN"
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
|
"app.security.admin-user=admin",
|
||||||
|
"app.security.admin-password={noop}very-secret",
|
||||||
|
"app.security.enable-tokens=true"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(name = "configsUpdateProducer", types = [MockSink::class])
|
@MockBean(name = "configsUpdateProducer", classes = [MockSink::class])
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [
|
Generator::class,
|
||||||
Generator::class,
|
MtbFileSender::class,
|
||||||
MtbFileSender::class,
|
ConnectionCheckService::class,
|
||||||
RequestProcessor::class,
|
RequestProcessor::class,
|
||||||
TransformationService::class,
|
TransformationService::class,
|
||||||
GPasConnectionCheckService::class,
|
TokenRepository::class,
|
||||||
RestConnectionCheckService::class,
|
RestConnectionCheckService::class
|
||||||
GIcsConnectionCheckService::class
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
class ConfigControllerTest {
|
class ConfigControllerTest {
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
private lateinit var mockMvc: MockMvc
|
||||||
private lateinit var webClient: WebClient
|
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
private lateinit var connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Autowired mockMvc: MockMvc,
|
@Autowired mockMvc: MockMvc,
|
||||||
@Autowired requestProcessor: RequestProcessor,
|
@Autowired requestProcessor: RequestProcessor
|
||||||
@Autowired connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
|
||||||
) {
|
) {
|
||||||
this.mockMvc = mockMvc
|
this.mockMvc = mockMvc
|
||||||
this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
|
|
||||||
this.requestProcessor = requestProcessor
|
this.requestProcessor = requestProcessor
|
||||||
this.connectionCheckUpdateProducer = connectionCheckUpdateProducer
|
|
||||||
|
|
||||||
webClient.options.isThrowExceptionOnScriptError = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShouldRequestConfigPageIfLoggedIn() {
|
fun testShouldShowConfigPageIfLoggedIn() {
|
||||||
mockMvc.get("/configs") {
|
mockMvc.get("/configs") {
|
||||||
with(user("admin").roles("ADMIN"))
|
with(user("admin").roles("ADMIN"))
|
||||||
accept(MediaType.TEXT_HTML)
|
accept(MediaType.TEXT_HTML)
|
||||||
}.andExpect {
|
}.andExpect {
|
||||||
status { isOk() }
|
status { isOk() }
|
||||||
view { name("configs") }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,244 +113,4 @@ class ConfigControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.security.enable-tokens=true",
|
|
||||||
"app.security.admin-user=admin"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@MockitoBean(
|
|
||||||
types = [
|
|
||||||
TokenService::class
|
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class WithTokensEnabled {
|
|
||||||
private lateinit var tokenService: TokenService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Autowired tokenService: TokenService
|
|
||||||
) {
|
|
||||||
webClient.options.isThrowExceptionOnScriptError = false
|
|
||||||
|
|
||||||
this.tokenService = tokenService
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldSaveNewToken() {
|
|
||||||
mockMvc.post("/configs/tokens") {
|
|
||||||
with(user("admin").roles("ADMIN"))
|
|
||||||
accept(MediaType.TEXT_HTML)
|
|
||||||
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
|
||||||
content = "name=Testtoken"
|
|
||||||
}.andExpect {
|
|
||||||
status { is2xxSuccessful() }
|
|
||||||
view { name("configs/tokens") }
|
|
||||||
}
|
|
||||||
|
|
||||||
val captor = argumentCaptor<String>()
|
|
||||||
verify(tokenService, times(1)).addToken(captor.capture())
|
|
||||||
|
|
||||||
assertThat(captor.firstValue).isEqualTo("Testtoken")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldNotSaveTokenWithExstingName() {
|
|
||||||
whenever(tokenService.addToken(anyString())).thenReturn(
|
|
||||||
Result.failure(
|
|
||||||
RuntimeException(
|
|
||||||
"Testfailure"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
mockMvc.post("/configs/tokens") {
|
|
||||||
with(user("admin").roles("ADMIN"))
|
|
||||||
accept(MediaType.TEXT_HTML)
|
|
||||||
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
|
||||||
content = "name=Testtoken"
|
|
||||||
}.andExpect {
|
|
||||||
status { is2xxSuccessful() }
|
|
||||||
view { name("configs/tokens") }
|
|
||||||
}
|
|
||||||
|
|
||||||
val captor = argumentCaptor<String>()
|
|
||||||
verify(tokenService, times(1)).addToken(captor.capture())
|
|
||||||
|
|
||||||
assertThat(captor.firstValue).isEqualTo("Testtoken")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldDeleteToken() {
|
|
||||||
mockMvc.delete("/configs/tokens/42") {
|
|
||||||
with(user("admin").roles("ADMIN"))
|
|
||||||
accept(MediaType.TEXT_HTML)
|
|
||||||
}.andExpect {
|
|
||||||
status { is2xxSuccessful() }
|
|
||||||
view { name("configs/tokens") }
|
|
||||||
}
|
|
||||||
|
|
||||||
val captor = argumentCaptor<Long>()
|
|
||||||
verify(tokenService, times(1)).deleteToken(captor.capture())
|
|
||||||
|
|
||||||
assertThat(captor.firstValue).isEqualTo(42)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldRenderConfigPageWithTokens() {
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/configs")
|
|
||||||
assertThat(
|
|
||||||
page.getElementById("tokens")
|
|
||||||
).isNotNull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.security.enable-tokens=false"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class WithTokensDisabled {
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
webClient.options.isThrowExceptionOnScriptError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldRenderConfigPageWithoutTokens() {
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/configs")
|
|
||||||
assertThat(
|
|
||||||
page.getElementById("tokens")
|
|
||||||
).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.security.enable-tokens=false",
|
|
||||||
"app.security.admin-user=admin",
|
|
||||||
"app.security.admin-password={noop}very-secret"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@MockitoBean(
|
|
||||||
types = [
|
|
||||||
UserRoleService::class
|
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class WithUserRolesEnabled {
|
|
||||||
private lateinit var userRoleService: UserRoleService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Autowired userRoleService: UserRoleService
|
|
||||||
) {
|
|
||||||
webClient.options.isThrowExceptionOnScriptError = false
|
|
||||||
|
|
||||||
this.userRoleService = userRoleService
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldDeleteUserRole() {
|
|
||||||
mockMvc.delete("/configs/userroles/42") {
|
|
||||||
with(user("admin").roles("ADMIN"))
|
|
||||||
accept(MediaType.TEXT_HTML)
|
|
||||||
}.andExpect {
|
|
||||||
status { is2xxSuccessful() }
|
|
||||||
view { name("configs/userroles") }
|
|
||||||
}
|
|
||||||
|
|
||||||
val captor = argumentCaptor<Long>()
|
|
||||||
verify(userRoleService, times(1)).deleteUserRole(captor.capture())
|
|
||||||
|
|
||||||
assertThat(captor.firstValue).isEqualTo(42)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldUpdateUserRole() {
|
|
||||||
mockMvc.put("/configs/userroles/42") {
|
|
||||||
with(user("admin").roles("ADMIN"))
|
|
||||||
accept(MediaType.TEXT_HTML)
|
|
||||||
contentType = MediaType.APPLICATION_FORM_URLENCODED
|
|
||||||
content = "role=ADMIN"
|
|
||||||
}.andExpect {
|
|
||||||
status { is2xxSuccessful() }
|
|
||||||
view { name("configs/userroles") }
|
|
||||||
}
|
|
||||||
|
|
||||||
val idCaptor = argumentCaptor<Long>()
|
|
||||||
val roleCaptor = argumentCaptor<Role>()
|
|
||||||
verify(userRoleService, times(1)).updateUserRole(
|
|
||||||
idCaptor.capture(),
|
|
||||||
roleCaptor.capture()
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(idCaptor.firstValue).isEqualTo(42)
|
|
||||||
assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldRenderConfigPageWithUserRoles() {
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/configs")
|
|
||||||
assertThat(
|
|
||||||
page.getElementById("userroles")
|
|
||||||
).isNotNull
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class WithUserRolesDisabled {
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
webClient.options.isThrowExceptionOnScriptError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRenderConfigPageWithoutUserRoles() {
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/configs")
|
|
||||||
assertThat(
|
|
||||||
page.getElementById("userroles")
|
|
||||||
).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class SseTest {
|
|
||||||
private lateinit var webClient: WebTestClient
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
applicationContext: WebApplicationContext
|
|
||||||
) {
|
|
||||||
this.webClient = MockMvcWebTestClient
|
|
||||||
.bindToApplicationContext(applicationContext).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestGPasSSE() {
|
|
||||||
val expectedEvent =
|
|
||||||
ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
|
|
||||||
|
|
||||||
connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
|
|
||||||
connectionCheckUpdateProducer.emitComplete { _, _ -> true }
|
|
||||||
|
|
||||||
val result =
|
|
||||||
webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM)
|
|
||||||
.exchange()
|
|
||||||
.expectStatus().isOk()
|
|
||||||
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
|
||||||
.returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
|
|
||||||
|
|
||||||
StepVerifier.create(result.responseBody)
|
|
||||||
.expectNext(expectedEvent)
|
|
||||||
.expectComplete()
|
|
||||||
.verify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,287 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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.web
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.*
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Report
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
|
||||||
import dev.dnpm.etl.processor.services.RequestService
|
|
||||||
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.Nested
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.assertThrows
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
|
||||||
import org.mockito.kotlin.any
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
|
||||||
import org.springframework.data.domain.Page
|
|
||||||
import org.springframework.data.domain.PageImpl
|
|
||||||
import org.springframework.data.domain.Pageable
|
|
||||||
import org.springframework.security.test.context.support.WithMockUser
|
|
||||||
import org.springframework.test.context.ContextConfiguration
|
|
||||||
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.web.servlet.MockMvc
|
|
||||||
import org.springframework.test.web.servlet.get
|
|
||||||
import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@WebMvcTest(controllers = [HomeController::class])
|
|
||||||
@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
|
|
||||||
@ContextConfiguration(
|
|
||||||
classes = [
|
|
||||||
HomeController::class,
|
|
||||||
AppConfiguration::class,
|
|
||||||
AppSecurityConfiguration::class
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
|
||||||
"app.security.admin-user=admin",
|
|
||||||
"app.security.admin-password={noop}very-secret"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@MockitoBean(
|
|
||||||
types = [RequestService::class]
|
|
||||||
)
|
|
||||||
class HomeControllerTest {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
private lateinit var webClient: WebClient
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Autowired mockMvc: MockMvc,
|
|
||||||
@Autowired requestService: RequestService
|
|
||||||
) {
|
|
||||||
this.mockMvc = mockMvc
|
|
||||||
this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
|
|
||||||
|
|
||||||
whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestHomePage() {
|
|
||||||
mockMvc.get("/").andExpect {
|
|
||||||
status { isOk() }
|
|
||||||
view { name("index") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class WithRequests {
|
|
||||||
|
|
||||||
private lateinit var requestService: RequestService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Autowired requestService: RequestService
|
|
||||||
) {
|
|
||||||
this.requestService = requestService
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldShowHomePage() {
|
|
||||||
whenever(requestService.findAll(any<Pageable>())).thenReturn(
|
|
||||||
PageImpl(
|
|
||||||
listOf(
|
|
||||||
Request(
|
|
||||||
2L,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("PSEUDO1"),
|
|
||||||
PatientId("PATIENT1"),
|
|
||||||
Fingerprint("ashdkasdh"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.SUCCESS
|
|
||||||
),
|
|
||||||
Request(
|
|
||||||
1L,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("PSEUDO1"),
|
|
||||||
PatientId("PATIENT1"),
|
|
||||||
Fingerprint("asdasdasd"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.ERROR
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/")
|
|
||||||
assertThat(page.querySelectorAll("tbody tr")).hasSize(2)
|
|
||||||
assertThat(page.querySelectorAll("div.notification.info")).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldShowRequestDetails() {
|
|
||||||
val requestId = randomRequestId()
|
|
||||||
|
|
||||||
whenever(requestService.findByUuid(anyValueClass())).thenReturn(
|
|
||||||
Optional.of(
|
|
||||||
Request(
|
|
||||||
2L,
|
|
||||||
requestId,
|
|
||||||
PatientPseudonym("PSEUDO1"),
|
|
||||||
PatientId("PATIENT1"),
|
|
||||||
Fingerprint("ashdkasdh"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.SUCCESS,
|
|
||||||
Instant.now(),
|
|
||||||
Report("Test")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}")
|
|
||||||
assertThat(page.querySelectorAll("tbody tr")).hasSize(1)
|
|
||||||
assertThat(page.querySelectorAll("div.notification.info")).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldShowPatientDetails() {
|
|
||||||
whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn(
|
|
||||||
PageImpl(
|
|
||||||
listOf(
|
|
||||||
Request(
|
|
||||||
2L,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("PSEUDO1"),
|
|
||||||
PatientId("PATIENT1"),
|
|
||||||
Fingerprint("ashdkasdh"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.SUCCESS
|
|
||||||
),
|
|
||||||
Request(
|
|
||||||
1L,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("PSEUDO1"),
|
|
||||||
PatientId("PATIENT1"),
|
|
||||||
Fingerprint("asdasdasd"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.ERROR
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1")
|
|
||||||
assertThat(page.querySelectorAll("tbody tr")).hasSize(2)
|
|
||||||
assertThat(page.querySelectorAll("div.notification.info")).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldShowPatientPseudonym() {
|
|
||||||
whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn(
|
|
||||||
PageImpl(
|
|
||||||
listOf(
|
|
||||||
Request(
|
|
||||||
2L,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("PSEUDO1"),
|
|
||||||
PatientId("PATIENT1"),
|
|
||||||
Fingerprint("ashdkasdh"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.SUCCESS
|
|
||||||
),
|
|
||||||
Request(
|
|
||||||
1L,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("PSEUDO1"),
|
|
||||||
PatientId("PATIENT1"),
|
|
||||||
Fingerprint("asdasdasd"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.ERROR
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1")
|
|
||||||
assertThat(page.querySelectorAll("h2 > span")).hasSize(1)
|
|
||||||
assertThat(page.querySelectorAll("h2 > span").first().textContent).isEqualTo("PSEUDO1")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class WithoutRequests {
|
|
||||||
|
|
||||||
private lateinit var requestService: RequestService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Autowired requestService: RequestService
|
|
||||||
) {
|
|
||||||
this.requestService = requestService
|
|
||||||
|
|
||||||
whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldShowHomePage() {
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/")
|
|
||||||
assertThat(page.querySelectorAll("tbody tr")).isEmpty()
|
|
||||||
assertThat(page.querySelectorAll("div.notification.info")).hasSize(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldThrowNotFoundExceptionForUnknownReport() {
|
|
||||||
val requestId = randomRequestId()
|
|
||||||
|
|
||||||
whenever(requestService.findByUuid(anyValueClass())).thenReturn(
|
|
||||||
Optional.empty()
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThrows<IOException> {
|
|
||||||
webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}")
|
|
||||||
}.also {
|
|
||||||
assertThat(it).hasRootCauseInstanceOf(NotFoundException::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@WithMockUser(username = "admin", roles = ["ADMIN"])
|
|
||||||
fun testShouldShowEmptyPatientDetails() {
|
|
||||||
whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn(Page.empty())
|
|
||||||
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1")
|
|
||||||
assertThat(page.querySelectorAll("tbody tr")).isEmpty()
|
|
||||||
assertThat(page.querySelectorAll("div.notification.info")).hasSize(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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.web
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
|
||||||
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.Test
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
|
||||||
import org.springframework.test.context.ContextConfiguration
|
|
||||||
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.web.servlet.MockMvc
|
|
||||||
import org.springframework.test.web.servlet.get
|
|
||||||
import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
|
|
||||||
|
|
||||||
@WebMvcTest(controllers = [LoginController::class])
|
|
||||||
@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
|
|
||||||
@ContextConfiguration(
|
|
||||||
classes = [
|
|
||||||
LoginController::class,
|
|
||||||
AppConfiguration::class,
|
|
||||||
AppSecurityConfiguration::class
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
|
||||||
"app.security.admin-user=admin",
|
|
||||||
"app.security.admin-password={noop}very-secret",
|
|
||||||
"app.security.enable-tokens=true"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@MockitoBean(
|
|
||||||
types = [TokenService::class]
|
|
||||||
)
|
|
||||||
class LoginControllerTest {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
private lateinit var webClient: WebClient
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(@Autowired mockMvc: MockMvc) {
|
|
||||||
this.mockMvc = mockMvc
|
|
||||||
this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestLoginPage() {
|
|
||||||
mockMvc.get("/login").andExpect {
|
|
||||||
status { isOk() }
|
|
||||||
view { name("login") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldShowLoginForm() {
|
|
||||||
val page = webClient.getPage<HtmlPage>("http://localhost/login")
|
|
||||||
assertThat(
|
|
||||||
page.getElementsByTagName("main").first().firstElementChild.getAttribute("class")
|
|
||||||
).isEqualTo("login-form")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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.web
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
|
||||||
import org.springframework.test.context.ContextConfiguration
|
|
||||||
import org.springframework.test.context.TestPropertySource
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
|
||||||
import org.springframework.test.web.servlet.get
|
|
||||||
import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
|
|
||||||
|
|
||||||
@WebMvcTest(controllers = [StatisticsController::class])
|
|
||||||
@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
|
|
||||||
@ContextConfiguration(
|
|
||||||
classes = [
|
|
||||||
StatisticsController::class,
|
|
||||||
AppConfiguration::class,
|
|
||||||
AppSecurityConfiguration::class
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
|
||||||
"app.security.admin-user=admin",
|
|
||||||
"app.security.admin-password={noop}very-secret"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
class StatisticsControllerTest {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
private lateinit var webClient: WebClient
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(@Autowired mockMvc: MockMvc) {
|
|
||||||
this.mockMvc = mockMvc
|
|
||||||
this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestLoginPage() {
|
|
||||||
mockMvc.get("/statistics").andExpect {
|
|
||||||
status { isOk() }
|
|
||||||
view { name("statistics") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,314 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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.web
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.Fingerprint
|
|
||||||
import dev.dnpm.etl.processor.PatientId
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
|
||||||
import dev.dnpm.etl.processor.monitoring.CountedState
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
|
||||||
import dev.dnpm.etl.processor.randomRequestId
|
|
||||||
import dev.dnpm.etl.processor.services.RequestService
|
|
||||||
import org.hamcrest.Matchers.equalTo
|
|
||||||
import org.hamcrest.Matchers.hasSize
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
|
||||||
import org.mockito.kotlin.doAnswer
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
|
||||||
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
|
||||||
import org.springframework.test.context.ContextConfiguration
|
|
||||||
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.web.reactive.server.WebTestClient
|
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
|
||||||
import org.springframework.test.web.servlet.client.MockMvcWebTestClient
|
|
||||||
import org.springframework.test.web.servlet.get
|
|
||||||
import org.springframework.web.context.WebApplicationContext
|
|
||||||
import reactor.core.publisher.Sinks
|
|
||||||
import reactor.test.StepVerifier
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
|
|
||||||
|
|
||||||
@WebMvcTest(controllers = [StatisticsRestController::class])
|
|
||||||
@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
|
|
||||||
@ContextConfiguration(
|
|
||||||
classes = [
|
|
||||||
StatisticsRestController::class,
|
|
||||||
AppConfiguration::class,
|
|
||||||
AppSecurityConfiguration::class
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@TestPropertySource(
|
|
||||||
properties = [
|
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
|
||||||
"app.security.admin-user=admin",
|
|
||||||
"app.security.admin-password={noop}very-secret"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@MockitoBean(
|
|
||||||
types = [RequestService::class]
|
|
||||||
)
|
|
||||||
class StatisticsRestControllerTest {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
|
|
||||||
private lateinit var statisticsUpdateProducer: Sinks.Many<Any>
|
|
||||||
private lateinit var requestService: RequestService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Autowired mockMvc: MockMvc,
|
|
||||||
@Autowired statisticsUpdateProducer: Sinks.Many<Any>,
|
|
||||||
@Autowired requestService: RequestService
|
|
||||||
) {
|
|
||||||
this.mockMvc = mockMvc
|
|
||||||
this.statisticsUpdateProducer = statisticsUpdateProducer
|
|
||||||
this.requestService = requestService
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class RequestStatesTest {
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestStatesForMtbFiles() {
|
|
||||||
doAnswer { _ ->
|
|
||||||
listOf(
|
|
||||||
CountedState(42, RequestStatus.WARNING),
|
|
||||||
CountedState(1, RequestStatus.UNKNOWN)
|
|
||||||
)
|
|
||||||
}.whenever(requestService).countStates()
|
|
||||||
|
|
||||||
mockMvc.get("/statistics/requeststates").andExpect {
|
|
||||||
status { isOk() }.also {
|
|
||||||
jsonPath("$", hasSize<Int>(2))
|
|
||||||
jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name))
|
|
||||||
jsonPath("$[0].value", equalTo(42))
|
|
||||||
jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name))
|
|
||||||
jsonPath("$[1].value", equalTo(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestStatesForDeletes() {
|
|
||||||
doAnswer { _ ->
|
|
||||||
listOf(
|
|
||||||
CountedState(42, RequestStatus.SUCCESS),
|
|
||||||
CountedState(1, RequestStatus.ERROR)
|
|
||||||
)
|
|
||||||
}.whenever(requestService).countDeleteStates()
|
|
||||||
|
|
||||||
mockMvc.get("/statistics/requeststates?delete=true").andExpect {
|
|
||||||
status { isOk() }.also {
|
|
||||||
jsonPath("$", hasSize<Int>(2))
|
|
||||||
jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name))
|
|
||||||
jsonPath("$[0].value", equalTo(42))
|
|
||||||
jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name))
|
|
||||||
jsonPath("$[1].value", equalTo(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class PatientRequestStatesTest {
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestPatientStatesForMtbFiles() {
|
|
||||||
doAnswer { _ ->
|
|
||||||
listOf(
|
|
||||||
CountedState(42, RequestStatus.WARNING),
|
|
||||||
CountedState(1, RequestStatus.UNKNOWN)
|
|
||||||
)
|
|
||||||
}.whenever(requestService).findPatientUniqueStates()
|
|
||||||
|
|
||||||
mockMvc.get("/statistics/requestpatientstates").andExpect {
|
|
||||||
status { isOk() }.also {
|
|
||||||
jsonPath("$", hasSize<Int>(2))
|
|
||||||
jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name))
|
|
||||||
jsonPath("$[0].value", equalTo(42))
|
|
||||||
jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name))
|
|
||||||
jsonPath("$[1].value", equalTo(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestPatientStatesForDeletes() {
|
|
||||||
doAnswer { _ ->
|
|
||||||
listOf(
|
|
||||||
CountedState(42, RequestStatus.SUCCESS),
|
|
||||||
CountedState(1, RequestStatus.ERROR)
|
|
||||||
)
|
|
||||||
}.whenever(requestService).findPatientUniqueDeleteStates()
|
|
||||||
|
|
||||||
mockMvc.get("/statistics/requestpatientstates?delete=true").andExpect {
|
|
||||||
status { isOk() }.also {
|
|
||||||
jsonPath("$", hasSize<Int>(2))
|
|
||||||
jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name))
|
|
||||||
jsonPath("$[0].value", equalTo(42))
|
|
||||||
jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name))
|
|
||||||
jsonPath("$[1].value", equalTo(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class LastMonthStatesTest {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
val zoneId = ZoneId.of("Europe/Berlin")
|
|
||||||
doAnswer { _ ->
|
|
||||||
listOf(
|
|
||||||
Request(
|
|
||||||
1,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("TEST_12345678901"),
|
|
||||||
PatientId("P1"),
|
|
||||||
Fingerprint("0123456789abcdef1"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.SUCCESS,
|
|
||||||
Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS).toInstant()
|
|
||||||
),
|
|
||||||
Request(
|
|
||||||
2,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("TEST_12345678902"),
|
|
||||||
PatientId("P2"),
|
|
||||||
Fingerprint("0123456789abcdef2"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.WARNING,
|
|
||||||
Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS).toInstant()
|
|
||||||
),
|
|
||||||
Request(
|
|
||||||
3,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("TEST_12345678901"),
|
|
||||||
PatientId("P2"),
|
|
||||||
Fingerprint("0123456789abcdee1"),
|
|
||||||
RequestType.DELETE,
|
|
||||||
RequestStatus.ERROR,
|
|
||||||
Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS).toInstant()
|
|
||||||
),
|
|
||||||
Request(
|
|
||||||
4,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("TEST_12345678902"),
|
|
||||||
PatientId("P2"),
|
|
||||||
Fingerprint("0123456789abcdef2"),
|
|
||||||
RequestType.MTB_FILE,
|
|
||||||
RequestStatus.DUPLICATION,
|
|
||||||
Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS).toInstant()
|
|
||||||
),
|
|
||||||
Request(
|
|
||||||
5,
|
|
||||||
randomRequestId(),
|
|
||||||
PatientPseudonym("TEST_12345678902"),
|
|
||||||
PatientId("P2"),
|
|
||||||
Fingerprint("0123456789abcdef2"),
|
|
||||||
RequestType.DELETE,
|
|
||||||
RequestStatus.UNKNOWN,
|
|
||||||
Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).toInstant()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}.whenever(requestService).findAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestLastMonthForMtbFiles() {
|
|
||||||
mockMvc.get("/statistics/requestslastmonth").andExpect {
|
|
||||||
status { isOk() }.also {
|
|
||||||
jsonPath("$", hasSize<Int>(31))
|
|
||||||
}.also {
|
|
||||||
jsonPath("$[28].nameValues.error", equalTo(0))
|
|
||||||
jsonPath("$[28].nameValues.warning", equalTo(1))
|
|
||||||
jsonPath("$[28].nameValues.success", equalTo(1))
|
|
||||||
jsonPath("$[28].nameValues.duplication", equalTo(0))
|
|
||||||
jsonPath("$[28].nameValues.unknown", equalTo(0))
|
|
||||||
jsonPath("$[29].nameValues.error", equalTo(0))
|
|
||||||
jsonPath("$[29].nameValues.warning", equalTo(0))
|
|
||||||
jsonPath("$[29].nameValues.success", equalTo(0))
|
|
||||||
jsonPath("$[29].nameValues.duplication", equalTo(1))
|
|
||||||
jsonPath("$[29].nameValues.unknown", equalTo(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestLastMonthForDeletes() {
|
|
||||||
mockMvc.get("/statistics/requestslastmonth?delete=true").andExpect {
|
|
||||||
status { isOk() }.also {
|
|
||||||
jsonPath("$", hasSize<Int>(31))
|
|
||||||
}.also {
|
|
||||||
jsonPath("$[29].nameValues.error", equalTo(1))
|
|
||||||
jsonPath("$[29].nameValues.warning", equalTo(0))
|
|
||||||
jsonPath("$[29].nameValues.success", equalTo(0))
|
|
||||||
jsonPath("$[29].nameValues.duplication", equalTo(0))
|
|
||||||
jsonPath("$[29].nameValues.unknown", equalTo(0))
|
|
||||||
jsonPath("$[30].nameValues.error", equalTo(0))
|
|
||||||
jsonPath("$[30].nameValues.warning", equalTo(0))
|
|
||||||
jsonPath("$[30].nameValues.success", equalTo(0))
|
|
||||||
jsonPath("$[30].nameValues.duplication", equalTo(0))
|
|
||||||
jsonPath("$[30].nameValues.unknown", equalTo(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class SseTest {
|
|
||||||
private lateinit var webClient: WebTestClient
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
applicationContext: WebApplicationContext,
|
|
||||||
) {
|
|
||||||
this.webClient = MockMvcWebTestClient
|
|
||||||
.bindToApplicationContext(applicationContext).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testShouldRequestSSE() {
|
|
||||||
statisticsUpdateProducer.emitComplete { _, _ -> true }
|
|
||||||
|
|
||||||
val result = webClient.get().uri("http://localhost/statistics/events").accept(TEXT_EVENT_STREAM).exchange()
|
|
||||||
.expectStatus().isOk()
|
|
||||||
.expectHeader().contentType(TEXT_EVENT_STREAM)
|
|
||||||
.returnResult(String::class.java)
|
|
||||||
|
|
||||||
StepVerifier.create(result.responseBody)
|
|
||||||
.expectComplete()
|
|
||||||
.verify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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,23 +21,46 @@ 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.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||||
|
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.Registry;
|
||||||
|
import org.apache.hc.core5.http.config.RegistryBuilder;
|
||||||
import org.hl7.fhir.r4.model.Identifier;
|
import org.hl7.fhir.r4.model.Identifier;
|
||||||
import org.hl7.fhir.r4.model.Parameters;
|
import org.hl7.fhir.r4.model.Parameters;
|
||||||
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.retry.support.RetryTemplate;
|
import org.springframework.retry.support.RetryTemplate;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
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,15 +69,27 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
|
private SSLContext customSslContext;
|
||||||
RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
|
|
||||||
|
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate) {
|
||||||
this.retryTemplate = retryTemplate;
|
this.retryTemplate = retryTemplate;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = getRestTemplete();
|
||||||
|
|
||||||
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());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (StringUtils.isNotBlank(gpasCfg.getSslCaLocation())) {
|
||||||
|
customSslContext = getSslContext(gpasCfg.getSslCaLocation());
|
||||||
|
log.warn(String.format("%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.",
|
||||||
|
this.getClass().getName(), gpasCfg.getSslCaLocation()));
|
||||||
|
}
|
||||||
|
} catch (IOException | KeyManagementException | KeyStoreException | CertificateException |
|
||||||
|
NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
log.debug(String.format("%s has been initialized", this.getClass().getName()));
|
log.debug(String.format("%s has been initialized", this.getClass().getName()));
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -100,6 +135,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) {
|
||||||
|
|
||||||
@ -144,7 +180,67 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.setBasicAuth(gPasUserName, gPasPassword);
|
String authHeader = gPasUserName + ":" + gPasPassword;
|
||||||
|
byte[] authHeaderBytes = authHeader.getBytes();
|
||||||
|
byte[] encodedAuthHeaderBytes = Base64.getEncoder().encode(authHeaderBytes);
|
||||||
|
String encodedAuthHeader = new String(encodedAuthHeaderBytes);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(gPasUserName) && StringUtils.isNotBlank(gPasPassword)) {
|
||||||
|
headers.set("Authorization", "Basic " + encodedAuthHeader);
|
||||||
|
}
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read SSL root certificate and return SSLContext
|
||||||
|
*
|
||||||
|
* @param certificateLocation file location to root certificate (PEM)
|
||||||
|
* @return initialized SSLContext
|
||||||
|
* @throws IOException file cannot be read
|
||||||
|
* @throws CertificateException in case we have an invalid certificate of type X.509
|
||||||
|
* @throws KeyStoreException keystore cannot be initialized
|
||||||
|
* @throws NoSuchAlgorithmException missing trust manager algorithmus
|
||||||
|
* @throws KeyManagementException key management failed at init SSLContext
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected SSLContext getSslContext(String certificateLocation)
|
||||||
|
throws IOException, CertificateException, KeyStoreException, KeyManagementException, NoSuchAlgorithmException {
|
||||||
|
|
||||||
|
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
|
|
||||||
|
FileInputStream fis = new FileInputStream(certificateLocation);
|
||||||
|
X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(new BufferedInputStream(fis));
|
||||||
|
|
||||||
|
ks.load(null, null);
|
||||||
|
ks.setCertificateEntry(Integer.toString(1), ca);
|
||||||
|
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
tmf.init(ks);
|
||||||
|
|
||||||
|
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||||
|
sslContext.init(null, tmf.getTrustManagers(), null);
|
||||||
|
|
||||||
|
return sslContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected RestTemplate getRestTemplete() {
|
||||||
|
if (customSslContext == null) {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
final var sslsf = new SSLConnectionSocketFactory(customSslContext);
|
||||||
|
final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
|
||||||
|
.register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build();
|
||||||
|
|
||||||
|
final BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(
|
||||||
|
socketFactoryRegistry);
|
||||||
|
final CloseableHttpClient httpClient = HttpClients.custom()
|
||||||
|
.setConnectionManager(connectionManager).build();
|
||||||
|
|
||||||
|
final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(
|
||||||
|
httpClient);
|
||||||
|
return new RestTemplate(requestFactory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,66 +56,19 @@ 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?,
|
||||||
val username: String?,
|
|
||||||
val password: String?,
|
|
||||||
val isBwhc: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NAME = "app.rest"
|
const val NAME = "app.rest"
|
||||||
@ -120,8 +79,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,16 +20,13 @@
|
|||||||
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.consent.ICheckConsent
|
|
||||||
import dev.dnpm.etl.processor.consent.GicsConsentService
|
|
||||||
import dev.dnpm.etl.processor.monitoring.*
|
import dev.dnpm.etl.processor.monitoring.*
|
||||||
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
|
||||||
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
|
||||||
import dev.dnpm.etl.processor.security.TokenRepository
|
import dev.dnpm.etl.processor.services.TokenRepository
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.services.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.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -38,7 +35,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
|||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
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.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
|
||||||
@ -48,7 +44,6 @@ import org.springframework.retry.support.RetryTemplateBuilder
|
|||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
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.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 kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@ -60,8 +55,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 +68,10 @@ 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): Generator {
|
||||||
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig)
|
return GpasPseudonymGenerator(configProperties, retryTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
|
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
|
||||||
@ -91,6 +80,20 @@ class AppConfiguration {
|
|||||||
return AnonymizingGenerator()
|
return AnonymizingGenerator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@Bean
|
||||||
|
fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate): Generator {
|
||||||
|
return GpasPseudonymGenerator(configProperties, retryTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 +103,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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -123,8 +122,6 @@ class AppConfiguration {
|
|||||||
fun retryTemplate(configProperties: AppConfigProperties): RetryTemplate {
|
fun retryTemplate(configProperties: AppConfigProperties): RetryTemplate {
|
||||||
return RetryTemplateBuilder()
|
return RetryTemplateBuilder()
|
||||||
.notRetryOn(IllegalArgumentException::class.java)
|
.notRetryOn(IllegalArgumentException::class.java)
|
||||||
.notRetryOn(HttpClientErrorException.BadRequest::class.java)
|
|
||||||
.notRetryOn(HttpClientErrorException.UnprocessableEntity::class.java)
|
|
||||||
.exponentialBackoff(2.seconds.toJavaDuration(), 1.25, 5.seconds.toJavaDuration())
|
.exponentialBackoff(2.seconds.toJavaDuration(), 1.25, 5.seconds.toJavaDuration())
|
||||||
.customPolicy(SimpleRetryPolicy(configProperties.maxRetryAttempts))
|
.customPolicy(SimpleRetryPolicy(configProperties.maxRetryAttempts))
|
||||||
.withListener(object : RetryListener {
|
.withListener(object : RetryListener {
|
||||||
@ -176,37 +173,5 @@ class AppConfiguration {
|
|||||||
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
|
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun jdbcConfiguration(): AbstractJdbcConfiguration {
|
|
||||||
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,25 +0,0 @@
|
|||||||
package dev.dnpm.etl.processor.config
|
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.Fingerprint
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.core.convert.converter.Converter
|
|
||||||
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class AppJdbcConfiguration : AbstractJdbcConfiguration() {
|
|
||||||
override fun userConverters(): MutableList<*> {
|
|
||||||
return mutableListOf(StringToFingerprintConverter(), FingerprintToStringConverter())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StringToFingerprintConverter : Converter<String, Fingerprint> {
|
|
||||||
override fun convert(source: String): Fingerprint {
|
|
||||||
return Fingerprint(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FingerprintToStringConverter : Converter<Fingerprint, String> {
|
|
||||||
override fun convert(source: Fingerprint): String {
|
|
||||||
return source.value
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) 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,11 +21,9 @@ package dev.dnpm.etl.processor.config
|
|||||||
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
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.output.RestBwhcMtbFileSender
|
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
||||||
import dev.dnpm.etl.processor.output.RestDipMtbFileSender
|
|
||||||
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
|
||||||
@ -54,16 +52,10 @@ class AppRestConfiguration {
|
|||||||
fun restMtbFileSender(
|
fun restMtbFileSender(
|
||||||
restTemplate: RestTemplate,
|
restTemplate: RestTemplate,
|
||||||
restTargetProperties: RestTargetProperties,
|
restTargetProperties: RestTargetProperties,
|
||||||
retryTemplate: RetryTemplate,
|
retryTemplate: RetryTemplate
|
||||||
reportService: ReportService,
|
|
||||||
): MtbFileSender {
|
): MtbFileSender {
|
||||||
if (restTargetProperties.isBwhc) {
|
logger.info("Selected 'RestMtbFileSender'")
|
||||||
logger.info("Selected 'RestBwhcMtbFileSender'")
|
return RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
|
||||||
return RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Selected 'RestDipMtbFileSender'")
|
|
||||||
return RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -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,7 +21,7 @@ package dev.dnpm.etl.processor.config
|
|||||||
|
|
||||||
import dev.dnpm.etl.processor.security.UserRole
|
import dev.dnpm.etl.processor.security.UserRole
|
||||||
import dev.dnpm.etl.processor.security.UserRoleRepository
|
import dev.dnpm.etl.processor.security.UserRoleRepository
|
||||||
import dev.dnpm.etl.processor.security.UserRoleService
|
import dev.dnpm.etl.processor.services.UserRoleService
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
@ -44,8 +44,6 @@ import org.springframework.security.web.SecurityFilterChain
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
private const val LOGIN_PATH = "/login"
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(
|
@EnableConfigurationProperties(
|
||||||
value = [
|
value = [
|
||||||
@ -87,16 +85,11 @@ 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"))
|
||||||
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
||||||
authorize("*.css", permitAll)
|
authorize("*.css", permitAll)
|
||||||
authorize("*.ico", permitAll)
|
authorize("*.ico", permitAll)
|
||||||
@ -111,15 +104,15 @@ class AppSecurityConfiguration(
|
|||||||
realmName = "ETL-Processor"
|
realmName = "ETL-Processor"
|
||||||
}
|
}
|
||||||
formLogin {
|
formLogin {
|
||||||
loginPage = LOGIN_PATH
|
loginPage = "/login"
|
||||||
}
|
}
|
||||||
oauth2Login {
|
oauth2Login {
|
||||||
loginPage = LOGIN_PATH
|
loginPage = "/login"
|
||||||
}
|
}
|
||||||
sessionManagement {
|
sessionManagement {
|
||||||
sessionConcurrency {
|
sessionConcurrency {
|
||||||
maximumSessions = 1
|
maximumSessions = 1
|
||||||
expiredUrl = "$LOGIN_PATH?expired"
|
expiredUrl = "/login?expired"
|
||||||
}
|
}
|
||||||
sessionFixation {
|
sessionFixation {
|
||||||
newSession()
|
newSession()
|
||||||
@ -132,22 +125,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,9 +145,9 @@ 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"))
|
||||||
authorize("/report/**", hasRole("ADMIN"))
|
authorize("/report/**", hasRole("ADMIN"))
|
||||||
authorize(anyRequest, permitAll)
|
authorize(anyRequest, permitAll)
|
||||||
}
|
}
|
||||||
@ -171,7 +155,7 @@ class AppSecurityConfiguration(
|
|||||||
realmName = "ETL-Processor"
|
realmName = "ETL-Processor"
|
||||||
}
|
}
|
||||||
formLogin {
|
formLogin {
|
||||||
loginPage = LOGIN_PATH
|
loginPage = "/login"
|
||||||
}
|
}
|
||||||
csrf { disable() }
|
csrf { disable() }
|
||||||
}
|
}
|
||||||
|
@ -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,9 @@ 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.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,33 +33,13 @@ 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)
|
val firstRequestIdHeader = data.headers().headers("requestId")?.firstOrNull()
|
||||||
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 firstRequestIdHeader = record.headers().headers("requestId")?.firstOrNull()
|
|
||||||
val requestId = if (null != firstRequestIdHeader) {
|
val requestId = if (null != firstRequestIdHeader) {
|
||||||
RequestId(String(firstRequestIdHeader.value()))
|
String(firstRequestIdHeader.value())
|
||||||
} else {
|
} else {
|
||||||
RequestId("")
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
||||||
@ -77,20 +52,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(mtbFile.patient.id)
|
||||||
} else {
|
} else {
|
||||||
requestProcessor.processDeletion(
|
requestProcessor.processDeletion(mtbFile.patient.id, 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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,21 +21,15 @@ 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.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"])
|
||||||
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,50 +39,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<Void> {
|
||||||
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")
|
requestProcessor.processDeletion(mtbFile.patient.id)
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
|
||||||
requestProcessor.processDeletion(patientId, ttpConsentStatus)
|
|
||||||
}
|
}
|
||||||
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<Void> {
|
||||||
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)
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -20,25 +20,28 @@
|
|||||||
|
|
||||||
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
|
||||||
import org.apache.kafka.clients.consumer.Consumer
|
import org.apache.kafka.clients.consumer.Consumer
|
||||||
import org.apache.kafka.common.errors.TimeoutException
|
import org.apache.kafka.common.errors.TimeoutException
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.http.*
|
import org.springframework.http.HttpEntity
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.RequestEntity
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import org.springframework.web.util.UriComponentsBuilder
|
import org.springframework.web.util.UriComponentsBuilder
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
import java.time.Instant
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
fun interface ConnectionCheckService {
|
interface ConnectionCheckService {
|
||||||
|
|
||||||
fun connectionAvailable(): ConnectionCheckResult
|
fun connectionAvailable(): Boolean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,33 +51,9 @@ sealed class ConnectionCheckResult {
|
|||||||
|
|
||||||
abstract val available: Boolean
|
abstract val available: Boolean
|
||||||
|
|
||||||
abstract val timestamp: Instant
|
data class KafkaConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
|
||||||
|
data class RestConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
|
||||||
abstract val lastChange: Instant
|
data class GPasConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
|
||||||
|
|
||||||
data class KafkaConnectionCheckResult(
|
|
||||||
override val available: Boolean,
|
|
||||||
override val timestamp: Instant,
|
|
||||||
override val lastChange: Instant
|
|
||||||
) : ConnectionCheckResult()
|
|
||||||
|
|
||||||
data class RestConnectionCheckResult(
|
|
||||||
override val available: Boolean,
|
|
||||||
override val timestamp: Instant,
|
|
||||||
override val lastChange: Instant
|
|
||||||
) : ConnectionCheckResult()
|
|
||||||
|
|
||||||
data class GPasConnectionCheckResult(
|
|
||||||
override val available: Boolean,
|
|
||||||
override val timestamp: Instant,
|
|
||||||
override val lastChange: Instant
|
|
||||||
) : ConnectionCheckResult()
|
|
||||||
|
|
||||||
data class GIcsConnectionCheckResult(
|
|
||||||
override val available: Boolean,
|
|
||||||
override val timestamp: Instant,
|
|
||||||
override val lastChange: Instant
|
|
||||||
) : ConnectionCheckResult()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class KafkaConnectionCheckService(
|
class KafkaConnectionCheckService(
|
||||||
@ -83,33 +62,25 @@ class KafkaConnectionCheckService(
|
|||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
) : OutputConnectionCheckService {
|
) : OutputConnectionCheckService {
|
||||||
|
|
||||||
private var result = ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now())
|
private var connectionAvailable: Boolean = false
|
||||||
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
fun check() {
|
fun check() {
|
||||||
result = try {
|
connectionAvailable = try {
|
||||||
val available = null != consumer.listTopics(5.seconds.toJavaDuration())
|
null != consumer.listTopics(5.seconds.toJavaDuration())
|
||||||
ConnectionCheckResult.KafkaConnectionCheckResult(
|
} catch (e: TimeoutException) {
|
||||||
available,
|
false
|
||||||
Instant.now(),
|
|
||||||
if (result.available == available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
} catch (_: TimeoutException) {
|
|
||||||
ConnectionCheckResult.KafkaConnectionCheckResult(
|
|
||||||
false,
|
|
||||||
Instant.now(),
|
|
||||||
if (!result.available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
connectionCheckUpdateProducer.emitNext(
|
connectionCheckUpdateProducer.emitNext(
|
||||||
result,
|
ConnectionCheckResult.KafkaConnectionCheckResult(connectionAvailable),
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
Sinks.EmitFailureHandler.FAIL_FAST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult {
|
override fun connectionAvailable(): Boolean {
|
||||||
return this.result
|
return this.connectionAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -121,45 +92,27 @@ class RestConnectionCheckService(
|
|||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
) : OutputConnectionCheckService {
|
) : OutputConnectionCheckService {
|
||||||
|
|
||||||
private var result = ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now())
|
private var connectionAvailable: Boolean = false
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
fun check() {
|
fun check() {
|
||||||
result = try {
|
connectionAvailable = try {
|
||||||
val available = restTemplate.getForEntity(
|
restTemplate.getForEntity(
|
||||||
if (restTargetProperties.isBwhc) {
|
restTargetProperties.uri?.replace("/etl/api", "").toString(),
|
||||||
UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString()).path("").toUriString()
|
|
||||||
} else {
|
|
||||||
UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString())
|
|
||||||
.pathSegment("mtb")
|
|
||||||
.pathSegment("kaplan-meier")
|
|
||||||
.pathSegment("config")
|
|
||||||
.toUriString()
|
|
||||||
},
|
|
||||||
String::class.java
|
String::class.java
|
||||||
).statusCode == HttpStatus.OK
|
).statusCode == HttpStatus.OK
|
||||||
|
} catch (e: Exception) {
|
||||||
ConnectionCheckResult.RestConnectionCheckResult(
|
false
|
||||||
available,
|
|
||||||
Instant.now(),
|
|
||||||
if (result.available == available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
ConnectionCheckResult.RestConnectionCheckResult(
|
|
||||||
false,
|
|
||||||
Instant.now(),
|
|
||||||
if (!result.available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
connectionCheckUpdateProducer.emitNext(
|
connectionCheckUpdateProducer.emitNext(
|
||||||
result,
|
ConnectionCheckResult.RestConnectionCheckResult(connectionAvailable),
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
Sinks.EmitFailureHandler.FAIL_FAST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult {
|
override fun connectionAvailable(): Boolean {
|
||||||
return this.result
|
return this.connectionAvailable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,101 +123,40 @@ class GPasConnectionCheckService(
|
|||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
) : ConnectionCheckService {
|
) : ConnectionCheckService {
|
||||||
|
|
||||||
private var result = ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now())
|
private var connectionAvailable: Boolean = false
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
fun check() {
|
fun check() {
|
||||||
result = try {
|
connectionAvailable = try {
|
||||||
val uri = UriComponentsBuilder.fromUriString(
|
val uri = UriComponentsBuilder.fromUriString(
|
||||||
gPasConfigProperties.uri?.replace("/\$pseudonymizeAllowCreate", "/metadata").toString()
|
gPasConfigProperties.uri?.replace("/\$pseudonymizeAllowCreate", "/\$pseudonymize").toString()
|
||||||
).build().toUri()
|
)
|
||||||
|
.queryParam("target", gPasConfigProperties.target)
|
||||||
|
.queryParam("original", "???")
|
||||||
|
.build().toUri()
|
||||||
|
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = MediaType.APPLICATION_JSON
|
headers.contentType = MediaType.APPLICATION_JSON
|
||||||
if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) {
|
if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) {
|
||||||
headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password)
|
headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password)
|
||||||
}
|
}
|
||||||
|
restTemplate.exchange(
|
||||||
val available = restTemplate.exchange(
|
|
||||||
uri,
|
uri,
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
HttpEntity<Void>(headers),
|
HttpEntity<Void>(headers),
|
||||||
Void::class.java
|
Void::class.java
|
||||||
).statusCode == HttpStatus.OK
|
).statusCode == HttpStatus.OK
|
||||||
|
} catch (e: Exception) {
|
||||||
ConnectionCheckResult.GPasConnectionCheckResult(
|
false
|
||||||
available,
|
|
||||||
Instant.now(),
|
|
||||||
if (result.available == available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
ConnectionCheckResult.GPasConnectionCheckResult(
|
|
||||||
false,
|
|
||||||
Instant.now(),
|
|
||||||
if (!result.available) { result.lastChange } else { Instant.now() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
connectionCheckUpdateProducer.emitNext(
|
connectionCheckUpdateProducer.emitNext(
|
||||||
result,
|
ConnectionCheckResult.GPasConnectionCheckResult(connectionAvailable),
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
Sinks.EmitFailureHandler.FAIL_FAST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
|
override fun connectionAvailable(): Boolean {
|
||||||
return this.result
|
return this.connectionAvailable
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,15 +19,11 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.monitoring
|
package dev.dnpm.etl.processor.monitoring
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonAlias
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.fasterxml.jackson.annotation.JsonValue
|
import com.fasterxml.jackson.annotation.JsonValue
|
||||||
import com.fasterxml.jackson.core.JsonParseException
|
import com.fasterxml.jackson.core.JsonParseException
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException
|
import com.fasterxml.jackson.databind.JsonMappingException
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService.Issue
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService.Severity
|
|
||||||
import java.util.Optional
|
|
||||||
|
|
||||||
class ReportService(
|
class ReportService(
|
||||||
private val objectMapper: ObjectMapper
|
private val objectMapper: ObjectMapper
|
||||||
@ -58,11 +54,7 @@ class ReportService(
|
|||||||
private data class DataQualityReport(val issues: List<Issue>)
|
private data class DataQualityReport(val issues: List<Issue>)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Issue(
|
data class Issue(val severity: Severity, val message: String)
|
||||||
val severity: Severity,
|
|
||||||
@JsonAlias("details") val message: String,
|
|
||||||
val path: Optional<String> = Optional.empty()
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class Severity(@JsonValue val value: String) {
|
enum class Severity(@JsonValue val value: String) {
|
||||||
FATAL("fatal"),
|
FATAL("fatal"),
|
||||||
@ -70,13 +62,4 @@ class ReportService(
|
|||||||
WARNING("warning"),
|
WARNING("warning"),
|
||||||
INFO("info")
|
INFO("info")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun List<Issue>.asRequestStatus(): RequestStatus {
|
|
||||||
val severity = this.minOfOrNull { it.severity }
|
|
||||||
return when (severity) {
|
|
||||||
Severity.FATAL, Severity.ERROR -> RequestStatus.ERROR
|
|
||||||
Severity.WARNING -> RequestStatus.WARNING
|
|
||||||
else -> RequestStatus.SUCCESS
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -19,12 +19,10 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.monitoring
|
package dev.dnpm.etl.processor.monitoring
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.*
|
|
||||||
import org.springframework.data.annotation.Id
|
import org.springframework.data.annotation.Id
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.data.jdbc.repository.query.Query
|
import org.springframework.data.jdbc.repository.query.Query
|
||||||
import org.springframework.data.relational.core.mapping.Column
|
|
||||||
import org.springframework.data.relational.core.mapping.Embedded
|
import org.springframework.data.relational.core.mapping.Embedded
|
||||||
import org.springframework.data.relational.core.mapping.Table
|
import org.springframework.data.relational.core.mapping.Table
|
||||||
import org.springframework.data.repository.CrudRepository
|
import org.springframework.data.repository.CrudRepository
|
||||||
@ -32,48 +30,26 @@ import org.springframework.data.repository.PagingAndSortingRepository
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
typealias RequestId = UUID
|
||||||
|
|
||||||
@Table("request")
|
@Table("request")
|
||||||
data class Request(
|
data class Request(
|
||||||
@Id val id: Long? = null,
|
@Id val id: Long? = null,
|
||||||
val uuid: RequestId = randomRequestId(),
|
val uuid: String = RequestId.randomUUID().toString(),
|
||||||
val patientPseudonym: PatientPseudonym,
|
val patientId: String,
|
||||||
val pid: PatientId,
|
val pid: String,
|
||||||
@Column("fingerprint")
|
val fingerprint: String,
|
||||||
val fingerprint: Fingerprint,
|
|
||||||
val type: RequestType,
|
val type: RequestType,
|
||||||
var status: RequestStatus,
|
var status: RequestStatus,
|
||||||
var processedAt: Instant = Instant.now(),
|
var processedAt: Instant = Instant.now(),
|
||||||
@Embedded.Nullable var report: Report? = null
|
@Embedded.Nullable var report: Report? = null
|
||||||
) {
|
)
|
||||||
constructor(
|
|
||||||
uuid: RequestId,
|
|
||||||
patientPseudonym: PatientPseudonym,
|
|
||||||
pid: PatientId,
|
|
||||||
fingerprint: Fingerprint,
|
|
||||||
type: RequestType,
|
|
||||||
status: RequestStatus
|
|
||||||
) :
|
|
||||||
this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now())
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
uuid: RequestId,
|
|
||||||
patientPseudonym: PatientPseudonym,
|
|
||||||
pid: PatientId,
|
|
||||||
fingerprint: Fingerprint,
|
|
||||||
type: RequestType,
|
|
||||||
status: RequestStatus,
|
|
||||||
processedAt: Instant
|
|
||||||
) :
|
|
||||||
this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmRecord
|
|
||||||
data class Report(
|
data class Report(
|
||||||
val description: String,
|
val description: String,
|
||||||
val dataQualityReport: String = ""
|
val dataQualityReport: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmRecord
|
|
||||||
data class CountedState(
|
data class CountedState(
|
||||||
val count: Int,
|
val count: Int,
|
||||||
val status: RequestStatus,
|
val status: RequestStatus,
|
||||||
@ -81,17 +57,17 @@ data class CountedState(
|
|||||||
|
|
||||||
interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRepository<Request, Long> {
|
interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRepository<Request, Long> {
|
||||||
|
|
||||||
fun findAllByPatientPseudonymOrderByProcessedAtDesc(patientId: PatientPseudonym): List<Request>
|
fun findAllByPatientIdOrderByProcessedAtDesc(patientId: String): List<Request>
|
||||||
|
|
||||||
fun findByUuidEquals(uuid: RequestId): Optional<Request>
|
fun findByUuidEquals(uuid: String): Optional<Request>
|
||||||
|
|
||||||
fun findRequestByPatientPseudonym(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request>
|
fun findRequestByPatientId(patientId: String, pageable: Pageable): Page<Request>
|
||||||
|
|
||||||
@Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;")
|
@Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;")
|
||||||
fun countStates(): List<CountedState>
|
fun countStates(): List<CountedState>
|
||||||
|
|
||||||
@Query("SELECT count(*) AS count, status FROM (" +
|
@Query("SELECT count(*) AS count, status FROM (" +
|
||||||
"SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
|
"SELECT status, rank() OVER (PARTITION BY patient_id ORDER BY processed_at DESC) AS rank FROM request " +
|
||||||
"WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " +
|
"WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " +
|
||||||
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
|
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
|
||||||
fun findPatientUniqueStates(): List<CountedState>
|
fun findPatientUniqueStates(): List<CountedState>
|
||||||
@ -100,7 +76,7 @@ interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRep
|
|||||||
fun countDeleteStates(): List<CountedState>
|
fun countDeleteStates(): List<CountedState>
|
||||||
|
|
||||||
@Query("SELECT count(*) AS count, status FROM (" +
|
@Query("SELECT count(*) AS count, status FROM (" +
|
||||||
"SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
|
"SELECT status, rank() OVER (PARTITION BY patient_id ORDER BY processed_at DESC) AS rank FROM request " +
|
||||||
"WHERE type = 'DELETE'" +
|
"WHERE type = 'DELETE'" +
|
||||||
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
|
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
|
||||||
fun findPatientUniqueDeleteStates(): List<CountedState>
|
fun findPatientUniqueDeleteStates(): List<CountedState>
|
||||||
|
@ -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,9 @@ 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.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 +37,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,11 +58,11 @@ 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()
|
||||||
.withPatient(request.patientId.value)
|
.withPatient(request.patientId)
|
||||||
.withStatus(Consent.Status.REJECTED)
|
.withStatus(Consent.Status.REJECTED)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@ -79,15 +70,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),
|
objectMapper.writeValueAsString(Data(request.requestId, dummyMtbFile))
|
||||||
// Always use old BwhcV1FileRequest with Consent REJECT
|
)
|
||||||
objectMapper.writeValueAsString(BwhcV1MtbFileRequest(request.requestId, dummyMtbFile))
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = kafkaTemplate.send(record)
|
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
logger.debug("Sent deletion request via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@ -102,15 +90,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}\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Data(val requestId: String, val content: MtbFile)
|
||||||
|
}
|
@ -19,17 +19,23 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
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: String, val mtbFile: MtbFile)
|
||||||
|
|
||||||
|
data class DeleteRequest(val requestId: String, val patientId: String)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.asRequestStatus(): RequestStatus {
|
fun Int.asRequestStatus(): RequestStatus {
|
||||||
@ -43,4 +49,4 @@ fun Int.asRequestStatus(): RequestStatus {
|
|||||||
|
|
||||||
fun HttpStatusCode.asRequestStatus(): RequestStatus {
|
fun HttpStatusCode.asRequestStatus(): RequestStatus {
|
||||||
return this.value().asRequestStatus()
|
return this.value().asRequestStatus()
|
||||||
}
|
}
|
@ -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,51 +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 dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
import org.springframework.retry.support.RetryTemplate
|
|
||||||
import org.springframework.web.client.RestTemplate
|
|
||||||
import org.springframework.web.util.UriComponentsBuilder
|
|
||||||
|
|
||||||
class RestBwhcMtbFileSender(
|
|
||||||
restTemplate: RestTemplate,
|
|
||||||
private val restTargetProperties: RestTargetProperties,
|
|
||||||
retryTemplate: RetryTemplate,
|
|
||||||
reportService: ReportService,
|
|
||||||
) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) {
|
|
||||||
|
|
||||||
override fun sendUrl(): String {
|
|
||||||
return UriComponentsBuilder
|
|
||||||
.fromUriString(restTargetProperties.uri.toString())
|
|
||||||
.pathSegment("MTBFile")
|
|
||||||
.toUriString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteUrl(patientId: PatientPseudonym): String {
|
|
||||||
return UriComponentsBuilder
|
|
||||||
.fromUriString(restTargetProperties.uri.toString())
|
|
||||||
.pathSegment("Patient")
|
|
||||||
.pathSegment(patientId.value)
|
|
||||||
.toUriString()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,55 +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 dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
import org.springframework.retry.support.RetryTemplate
|
|
||||||
import org.springframework.web.client.RestTemplate
|
|
||||||
import org.springframework.web.util.UriComponentsBuilder
|
|
||||||
|
|
||||||
class RestDipMtbFileSender(
|
|
||||||
restTemplate: RestTemplate,
|
|
||||||
private val restTargetProperties: RestTargetProperties,
|
|
||||||
retryTemplate: RetryTemplate,
|
|
||||||
reportService: ReportService
|
|
||||||
) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) {
|
|
||||||
|
|
||||||
override fun sendUrl(): String {
|
|
||||||
return UriComponentsBuilder
|
|
||||||
.fromUriString(restTargetProperties.uri.toString())
|
|
||||||
.pathSegment("mtb")
|
|
||||||
.pathSegment("etl")
|
|
||||||
.pathSegment("patient-record")
|
|
||||||
.toUriString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteUrl(patientId: PatientPseudonym): String {
|
|
||||||
return UriComponentsBuilder
|
|
||||||
.fromUriString(restTargetProperties.uri.toString())
|
|
||||||
.pathSegment("mtb")
|
|
||||||
.pathSegment("etl")
|
|
||||||
.pathSegment("patient")
|
|
||||||
.pathSegment(patientId.value)
|
|
||||||
.toUriString()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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,71 +19,62 @@
|
|||||||
|
|
||||||
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.monitoring.asRequestStatus
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
import org.springframework.web.client.RestClientException
|
import org.springframework.web.client.RestClientException
|
||||||
import org.springframework.web.client.RestClientResponseException
|
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
abstract class RestMtbFileSender(
|
class RestMtbFileSender(
|
||||||
private val restTemplate: RestTemplate,
|
private val restTemplate: RestTemplate,
|
||||||
private val restTargetProperties: RestTargetProperties,
|
private val restTargetProperties: RestTargetProperties,
|
||||||
private val retryTemplate: RetryTemplate,
|
private val retryTemplate: RetryTemplate
|
||||||
private val reportService: ReportService
|
|
||||||
) : MtbFileSender {
|
) : MtbFileSender {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(RestMtbFileSender::class.java)
|
private val logger = LoggerFactory.getLogger(RestMtbFileSender::class.java)
|
||||||
|
|
||||||
abstract fun sendUrl(): String
|
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
||||||
|
|
||||||
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
|
||||||
|
|
||||||
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders(request)
|
val headers = HttpHeaders()
|
||||||
val entityReq = HttpEntity(request.content, headers)
|
headers.contentType = MediaType.APPLICATION_JSON
|
||||||
|
val entityReq = HttpEntity(request.mtbFile, headers)
|
||||||
val response = restTemplate.postForEntity(
|
val response = restTemplate.postForEntity(
|
||||||
sendUrl(),
|
"${restTargetProperties.uri}/MTBFile",
|
||||||
entityReq,
|
entityReq,
|
||||||
String::class.java
|
String::class.java
|
||||||
)
|
)
|
||||||
if (!response.statusCode.is2xxSuccessful) {
|
if (!response.statusCode.is2xxSuccessful) {
|
||||||
logger.warn("Error sending to remote system: {}", response.body)
|
logger.warn("Error sending to remote system: {}", response.body)
|
||||||
return@execute MtbFileSender.Response(
|
return@execute MtbFileSender.Response(
|
||||||
reportService.deserialize(response.body).asRequestStatus(),
|
response.statusCode.asRequestStatus(),
|
||||||
"Status-Code: ${response.statusCode.value()}"
|
"Status-Code: ${response.statusCode.value()}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.debug("Sent file via RestMtbFileSender")
|
logger.debug("Sent file via RestMtbFileSender")
|
||||||
return@execute MtbFileSender.Response(reportService.deserialize(response.body).asRequestStatus(), response.body.orEmpty())
|
return@execute MtbFileSender.Response(response.statusCode.asRequestStatus(), response.body.orEmpty())
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
logger.error("Not a valid URI to export to: '{}'", restTargetProperties.uri!!)
|
logger.error("Not a valid URI to export to: '{}'", restTargetProperties.uri!!)
|
||||||
} catch (e: RestClientResponseException) {
|
} catch (e: RestClientException) {
|
||||||
logger.info(restTargetProperties.uri!!.toString())
|
logger.info(restTargetProperties.uri!!.toString())
|
||||||
logger.error("Request data not accepted by remote system", e)
|
logger.error("Cannot send data to remote system", e)
|
||||||
return MtbFileSender.Response(reportService.deserialize(e.responseBodyAsString).asRequestStatus(), e.responseBodyAsString)
|
|
||||||
}
|
}
|
||||||
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 = HttpHeaders()
|
||||||
|
headers.contentType = MediaType.APPLICATION_JSON
|
||||||
val entityReq = HttpEntity(null, headers)
|
val entityReq = HttpEntity(null, headers)
|
||||||
restTemplate.delete(
|
restTemplate.delete(
|
||||||
deleteUrl(request.patientId),
|
"${restTargetProperties.uri}/Patient/${request.patientId}",
|
||||||
entityReq,
|
entityReq,
|
||||||
String::class.java
|
String::class.java
|
||||||
)
|
)
|
||||||
@ -103,22 +94,4 @@ abstract class RestMtbFileSender(
|
|||||||
return this.restTargetProperties.uri.orEmpty()
|
return this.restTargetProperties.uri.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHttpHeaders(request: MtbRequest): HttpHeaders {
|
}
|
||||||
val username = restTargetProperties.username
|
|
||||||
val password = restTargetProperties.password
|
|
||||||
val headers = HttpHeaders()
|
|
||||||
headers.contentType = when (request) {
|
|
||||||
is BwhcV1MtbFileRequest -> MediaType.APPLICATION_JSON
|
|
||||||
is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
|
||||||
else -> MediaType.APPLICATION_JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.setBasicAuth(username, password)
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.pseudonym
|
package dev.dnpm.etl.processor.pseudonym
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.PatientId
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties
|
import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties
|
||||||
|
|
||||||
class PseudonymizeService(
|
class PseudonymizeService(
|
||||||
@ -28,10 +26,10 @@ class PseudonymizeService(
|
|||||||
private val configProperties: PseudonymizeConfigProperties
|
private val configProperties: PseudonymizeConfigProperties
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun patientPseudonym(patientId: PatientId): PatientPseudonym {
|
fun patientPseudonym(patientId: String): String {
|
||||||
return when (generator) {
|
return when (generator) {
|
||||||
is GpasPseudonymGenerator -> PatientPseudonym(generator.generate(patientId.value))
|
is GpasPseudonymGenerator -> generator.generate(patientId)
|
||||||
else -> PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}")
|
else -> "${configProperties.prefix}_${generator.generate(patientId)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,18 +20,16 @@
|
|||||||
package dev.dnpm.etl.processor.pseudonym
|
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.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) {
|
||||||
val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value
|
val patientPseudonym = pseudonymizeService.patientPseudonym(this.patient.id)
|
||||||
|
|
||||||
this.episode?.patient = patientPseudonym
|
this.episode?.patient = patientPseudonym
|
||||||
this.carePlans?.forEach { it.patient = patientPseudonym }
|
this.carePlans?.forEach { it.patient = patientPseudonym }
|
||||||
@ -50,11 +48,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 +62,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) {
|
||||||
@ -124,8 +119,8 @@ infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService)
|
|||||||
id = id?.let { anonymize(it) }
|
id = id?.let { anonymize(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.geneticCounsellingRequests?.onEach { geneticCounsellingRequest ->
|
this.geneticCounsellingRequests?.onEach { geneticCounsellingRequest ->
|
||||||
geneticCounsellingRequest?.apply {
|
geneticCounsellingRequest?.apply {
|
||||||
id = id?.let { anonymize(it) }
|
id = id?.let { anonymize(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -227,101 +222,4 @@ infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService)
|
|||||||
id = id?.let { anonymize(it) }
|
id = id?.let { anonymize(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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
|
||||||
@ -21,29 +21,17 @@ package dev.dnpm.etl.processor.services
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
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.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,122 +45,36 @@ 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) {
|
||||||
processMtbFile(mtbFile, randomRequestId())
|
processMtbFile(mtbFile, UUID.randomUUID().toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
fun processMtbFile(mtbFile: MtbFile, requestId: String) {
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
val pid = 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 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,
|
uuid = requestId,
|
||||||
request.patientPseudonym(),
|
patientId = request.mtbFile.patient.id,
|
||||||
pid,
|
pid = pid,
|
||||||
fingerprint(request),
|
fingerprint = fingerprint(request.mtbFile),
|
||||||
RequestType.MTB_FILE,
|
status = RequestStatus.UNKNOWN,
|
||||||
RequestStatus.UNKNOWN
|
type = RequestType.MTB_FILE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
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,61 +86,47 @@ 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) {
|
||||||
RequestStatus.ERROR, RequestStatus.WARNING -> Optional.of(responseStatus.body)
|
RequestStatus.WARNING -> Optional.of(responseStatus.body)
|
||||||
else -> Optional.empty()
|
else -> Optional.empty()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean {
|
private fun isDuplication(pseudonymizedMtbFile: MtbFile): Boolean {
|
||||||
val patientPseudonym = when (pseudonymizedMtbFileRequest) {
|
|
||||||
is BwhcV1MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
|
||||||
is DnpmV2MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastMtbFileRequestForPatient =
|
val lastMtbFileRequestForPatient =
|
||||||
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
requestService.lastMtbFileRequestForPatientPseudonym(pseudonymizedMtbFile.patient.id)
|
||||||
val isLastRequestDeletion =
|
val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(pseudonymizedMtbFile.patient.id)
|
||||||
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: String) {
|
||||||
processDeletion(patientId, randomRequestId(), isConsented)
|
processDeletion(patientId, UUID.randomUUID().toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) {
|
fun processDeletion(patientId: String, requestId: String) {
|
||||||
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,
|
uuid = requestId,
|
||||||
patientPseudonym,
|
patientId = patientPseudonym,
|
||||||
patientId,
|
pid = patientId,
|
||||||
fingerprint(patientPseudonym.value),
|
fingerprint = fingerprint(patientPseudonym),
|
||||||
RequestType.DELETE,
|
status = RequestStatus.UNKNOWN,
|
||||||
requestStatus
|
type = RequestType.DELETE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym))
|
val responseStatus = sender.send(MtbFileSender.DeleteRequest(requestId, patientPseudonym))
|
||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
@ -256,9 +144,9 @@ class RequestProcessor(
|
|||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
uuid = requestId,
|
uuid = requestId,
|
||||||
patientPseudonym = emptyPatientPseudonym(),
|
patientId = "???",
|
||||||
pid = patientId,
|
pid = patientId,
|
||||||
fingerprint = Fingerprint.empty(),
|
fingerprint = "",
|
||||||
status = RequestStatus.ERROR,
|
status = RequestStatus.ERROR,
|
||||||
type = RequestType.DELETE,
|
type = RequestType.DELETE,
|
||||||
report = Report("Fehler bei der Pseudonymisierung")
|
report = Report("Fehler bei der Pseudonymisierung")
|
||||||
@ -267,19 +155,14 @@ class RequestProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint {
|
private fun fingerprint(mtbFile: MtbFile): String {
|
||||||
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): String {
|
||||||
return Fingerprint(
|
return Base32().encodeAsString(DigestUtils.sha256(s))
|
||||||
Base32().encodeAsString(DigestUtils.sha256(s))
|
.replace("=", "")
|
||||||
.replace("=", "")
|
.lowercase()
|
||||||
.lowercase()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -19,13 +19,11 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.monitoring.*
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.springframework.data.domain.Page
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import org.springframework.data.domain.Pageable
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class RequestService(
|
class RequestService(
|
||||||
@ -34,32 +32,15 @@ class RequestService(
|
|||||||
|
|
||||||
fun save(request: Request) = requestRepository.save(request)
|
fun save(request: Request) = requestRepository.save(request)
|
||||||
|
|
||||||
fun findAll(): Iterable<Request> = requestRepository.findAll()
|
fun allRequestsByPatientPseudonym(patientPseudonym: String) = requestRepository
|
||||||
|
.findAllByPatientIdOrderByProcessedAtDesc(patientPseudonym)
|
||||||
|
|
||||||
fun findAll(pageable: Pageable): Page<Request> = requestRepository.findAll(pageable)
|
fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: String) =
|
||||||
|
|
||||||
fun findByUuid(uuid: RequestId): Optional<Request> =
|
|
||||||
requestRepository.findByUuidEquals(uuid)
|
|
||||||
|
|
||||||
fun findRequestByPatientId(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request> = requestRepository.findRequestByPatientPseudonym(patientPseudonym, pageable)
|
|
||||||
|
|
||||||
fun allRequestsByPatientPseudonym(patientPseudonym: PatientPseudonym) = requestRepository
|
|
||||||
.findAllByPatientPseudonymOrderByProcessedAtDesc(patientPseudonym)
|
|
||||||
|
|
||||||
fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: PatientPseudonym) =
|
|
||||||
Companion.lastMtbFileRequestForPatientPseudonym(allRequestsByPatientPseudonym(patientPseudonym))
|
Companion.lastMtbFileRequestForPatientPseudonym(allRequestsByPatientPseudonym(patientPseudonym))
|
||||||
|
|
||||||
fun isLastRequestWithKnownStatusDeletion(patientPseudonym: PatientPseudonym) =
|
fun isLastRequestWithKnownStatusDeletion(patientPseudonym: String) =
|
||||||
Companion.isLastRequestWithKnownStatusDeletion(allRequestsByPatientPseudonym(patientPseudonym))
|
Companion.isLastRequestWithKnownStatusDeletion(allRequestsByPatientPseudonym(patientPseudonym))
|
||||||
|
|
||||||
fun countStates(): Iterable<CountedState> = requestRepository.countStates()
|
|
||||||
|
|
||||||
fun countDeleteStates(): Iterable<CountedState> = requestRepository.countDeleteStates()
|
|
||||||
|
|
||||||
fun findPatientUniqueStates(): List<CountedState> = requestRepository.findPatientUniqueStates()
|
|
||||||
|
|
||||||
fun findPatientUniqueDeleteStates(): List<CountedState> = requestRepository.findPatientUniqueDeleteStates()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = allRequests
|
fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = allRequests
|
||||||
|
@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Report
|
import dev.dnpm.etl.processor.monitoring.Report
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.context.event.EventListener
|
import org.springframework.context.event.EventListener
|
||||||
@ -31,7 +31,7 @@ import java.util.*
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class ResponseProcessor(
|
class ResponseProcessor(
|
||||||
private val requestService: RequestService,
|
private val requestRepository: RequestRepository,
|
||||||
private val statisticsUpdateProducer: Sinks.Many<Any>
|
private val statisticsUpdateProducer: Sinks.Many<Any>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ class ResponseProcessor(
|
|||||||
|
|
||||||
@EventListener(classes = [ResponseEvent::class])
|
@EventListener(classes = [ResponseEvent::class])
|
||||||
fun handleResponseEvent(event: ResponseEvent) {
|
fun handleResponseEvent(event: ResponseEvent) {
|
||||||
requestService.findByUuid(event.requestUuid).ifPresentOrElse({
|
requestRepository.findByUuidEquals(event.requestUuid).ifPresentOrElse({
|
||||||
it.processedAt = event.timestamp
|
it.processedAt = event.timestamp
|
||||||
it.status = event.status
|
it.status = event.status
|
||||||
|
|
||||||
@ -70,19 +70,13 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestService.save(it)
|
requestRepository.save(it)
|
||||||
|
|
||||||
statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST)
|
statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST)
|
||||||
}, {
|
}, {
|
||||||
@ -93,7 +87,7 @@ class ResponseProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ResponseEvent(
|
data class ResponseEvent(
|
||||||
val requestUuid: RequestId,
|
val requestUuid: String,
|
||||||
val timestamp: Instant,
|
val timestamp: Instant,
|
||||||
val status: RequestStatus,
|
val status: RequestStatus,
|
||||||
val body: Optional<String> = Optional.empty()
|
val body: Optional<String> = Optional.empty()
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package dev.dnpm.etl.processor.security
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct
|
import jakarta.annotation.PostConstruct
|
||||||
import org.springframework.data.annotation.Id
|
import org.springframework.data.annotation.Id
|
@ -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> {
|
||||||
|
@ -17,8 +17,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package dev.dnpm.etl.processor.security
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
|
import dev.dnpm.etl.processor.security.Role
|
||||||
|
import dev.dnpm.etl.processor.security.UserRole
|
||||||
|
import dev.dnpm.etl.processor.security.UserRoleRepository
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.security.core.session.SessionRegistry
|
import org.springframework.security.core.session.SessionRegistry
|
||||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser
|
@ -22,7 +22,6 @@ package dev.dnpm.etl.processor.services.kafka
|
|||||||
import com.fasterxml.jackson.annotation.JsonAlias
|
import com.fasterxml.jackson.annotation.JsonAlias
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.output.asRequestStatus
|
import dev.dnpm.etl.processor.output.asRequestStatus
|
||||||
import dev.dnpm.etl.processor.services.ResponseEvent
|
import dev.dnpm.etl.processor.services.ResponseEvent
|
||||||
@ -48,7 +47,7 @@ class KafkaResponseProcessor(
|
|||||||
Optional.empty()
|
Optional.empty()
|
||||||
}.ifPresentOrElse({ responseBody ->
|
}.ifPresentOrElse({ responseBody ->
|
||||||
val event = ResponseEvent(
|
val event = ResponseEvent(
|
||||||
RequestId(responseBody.requestId),
|
responseBody.requestId,
|
||||||
Instant.ofEpochMilli(data.timestamp()),
|
Instant.ofEpochMilli(data.timestamp()),
|
||||||
responseBody.statusCode.asRequestStatus(),
|
responseBody.statusCode.asRequestStatus(),
|
||||||
when (responseBody.statusCode.asRequestStatus()) {
|
when (responseBody.statusCode.asRequestStatus()) {
|
||||||
|
@ -1,63 +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
|
|
||||||
|
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Fingerprint(val value: String) {
|
|
||||||
override fun hashCode() = value.hashCode()
|
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is Fingerprint && other.value == value
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun empty() = Fingerprint("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
value class RequestId(val value: String) {
|
|
||||||
|
|
||||||
fun isBlank() = value.isBlank()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun randomRequestId() = RequestId(UUID.randomUUID().toString())
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
value class PatientId(val value: String)
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
value class PatientPseudonym(val value: String)
|
|
||||||
|
|
||||||
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,15 +19,18 @@
|
|||||||
|
|
||||||
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
|
||||||
import dev.dnpm.etl.processor.security.UserRole
|
import dev.dnpm.etl.processor.security.UserRole
|
||||||
import dev.dnpm.etl.processor.security.Token
|
import dev.dnpm.etl.processor.services.Token
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.services.TokenService
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
import dev.dnpm.etl.processor.security.UserRoleService
|
import dev.dnpm.etl.processor.services.UserRoleService
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.codec.ServerSentEvent
|
import org.springframework.http.codec.ServerSentEvent
|
||||||
@ -53,20 +56,16 @@ class ConfigController(
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
fun index(model: Model): String {
|
fun index(model: Model): String {
|
||||||
val outputConnectionAvailable =
|
val outputConnectionAvailable =
|
||||||
connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().first().connectionAvailable()
|
||||||
|
|
||||||
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) {
|
||||||
@ -146,11 +127,10 @@ class ConfigController(
|
|||||||
} else {
|
} else {
|
||||||
model.addAttribute("tokensEnabled", true)
|
model.addAttribute("tokensEnabled", true)
|
||||||
val result = tokenService.addToken(name)
|
val result = tokenService.addToken(name)
|
||||||
result.onSuccess {
|
if (result.isSuccess) {
|
||||||
model.addAttribute("newTokenValue", it)
|
model.addAttribute("newTokenValue", result.getOrDefault(""))
|
||||||
model.addAttribute("success", true)
|
model.addAttribute("success", true)
|
||||||
}
|
} else {
|
||||||
result.onFailure {
|
|
||||||
model.addAttribute("success", false)
|
model.addAttribute("success", false)
|
||||||
}
|
}
|
||||||
model.addAttribute("tokens", tokenService.findAll())
|
model.addAttribute("tokens", tokenService.findAll())
|
||||||
@ -202,14 +182,12 @@ class ConfigController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
|
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
|
||||||
@ResponseBody
|
|
||||||
fun events(): Flux<ServerSentEvent<Any>> {
|
fun events(): Flux<ServerSentEvent<Any>> {
|
||||||
return connectionCheckUpdateProducer.asFlux().map {
|
return connectionCheckUpdateProducer.asFlux().map {
|
||||||
val event = when (it) {
|
val event = when (it) {
|
||||||
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>()
|
||||||
|
@ -20,10 +20,9 @@
|
|||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.NotFoundException
|
import dev.dnpm.etl.processor.NotFoundException
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.services.RequestService
|
import dev.dnpm.etl.processor.monitoring.RequestId
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.data.domain.Sort
|
import org.springframework.data.domain.Sort
|
||||||
import org.springframework.data.web.PageableDefault
|
import org.springframework.data.web.PageableDefault
|
||||||
@ -36,7 +35,7 @@ import org.springframework.web.bind.annotation.RequestMapping
|
|||||||
@Controller
|
@Controller
|
||||||
@RequestMapping(path = ["/"])
|
@RequestMapping(path = ["/"])
|
||||||
class HomeController(
|
class HomeController(
|
||||||
private val requestService: RequestService,
|
private val requestRepository: RequestRepository,
|
||||||
private val reportService: ReportService
|
private val reportService: ReportService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -45,20 +44,20 @@ class HomeController(
|
|||||||
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
|
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
|
||||||
model: Model
|
model: Model
|
||||||
): String {
|
): String {
|
||||||
val requests = requestService.findAll(pageable)
|
val requests = requestRepository.findAll(pageable)
|
||||||
model.addAttribute("requests", requests)
|
model.addAttribute("requests", requests)
|
||||||
|
|
||||||
return "index"
|
return "index"
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(path = ["patient/{patientPseudonym}"])
|
@GetMapping(path = ["patient/{patientId}"])
|
||||||
fun byPatient(
|
fun byPatient(
|
||||||
@PathVariable patientPseudonym: PatientPseudonym,
|
@PathVariable patientId: String,
|
||||||
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
|
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
|
||||||
model: Model
|
model: Model
|
||||||
): String {
|
): String {
|
||||||
val requests = requestService.findRequestByPatientId(patientPseudonym, pageable)
|
val requests = requestRepository.findRequestByPatientId(patientId, pageable)
|
||||||
model.addAttribute("patientPseudonym", patientPseudonym.value)
|
model.addAttribute("patientId", patientId)
|
||||||
model.addAttribute("requests", requests)
|
model.addAttribute("requests", requests)
|
||||||
|
|
||||||
return "index"
|
return "index"
|
||||||
@ -66,7 +65,7 @@ class HomeController(
|
|||||||
|
|
||||||
@GetMapping(path = ["/report/{id}"])
|
@GetMapping(path = ["/report/{id}"])
|
||||||
fun report(@PathVariable id: RequestId, model: Model): String {
|
fun report(@PathVariable id: RequestId, model: Model): String {
|
||||||
val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException()
|
val request = requestRepository.findByUuidEquals(id.toString()).orElse(null) ?: throw NotFoundException()
|
||||||
model.addAttribute("request", request)
|
model.addAttribute("request", request)
|
||||||
model.addAttribute("issues", reportService.deserialize(request.report?.dataQualityReport))
|
model.addAttribute("issues", reportService.deserialize(request.report?.dataQualityReport))
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
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.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.services.RequestService
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.codec.ServerSentEvent
|
import org.springframework.http.codec.ServerSentEvent
|
||||||
@ -41,15 +41,15 @@ import java.time.temporal.ChronoUnit
|
|||||||
class StatisticsRestController(
|
class StatisticsRestController(
|
||||||
@Qualifier("statisticsUpdateProducer")
|
@Qualifier("statisticsUpdateProducer")
|
||||||
private val statisticsUpdateProducer: Sinks.Many<Any>,
|
private val statisticsUpdateProducer: Sinks.Many<Any>,
|
||||||
private val requestService: RequestService
|
private val requestRepository: RequestRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping(path = ["requeststates"])
|
@GetMapping(path = ["requeststates"])
|
||||||
fun requestStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
|
fun requestStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
|
||||||
val states = if (delete) {
|
val states = if (delete) {
|
||||||
requestService.countDeleteStates()
|
requestRepository.countDeleteStates()
|
||||||
} else {
|
} else {
|
||||||
requestService.countStates()
|
requestRepository.countStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
return states
|
return states
|
||||||
@ -79,7 +79,7 @@ class StatisticsRestController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Europe/Berlin"))
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Europe/Berlin"))
|
||||||
val data = requestService.findAll()
|
val data = requestRepository.findAll()
|
||||||
.filter { it.type == requestType }
|
.filter { it.type == requestType }
|
||||||
.filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) }
|
.filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) }
|
||||||
.groupBy { formatter.format(it.processedAt) }
|
.groupBy { formatter.format(it.processedAt) }
|
||||||
@ -115,9 +115,9 @@ class StatisticsRestController(
|
|||||||
@GetMapping(path = ["requestpatientstates"])
|
@GetMapping(path = ["requestpatientstates"])
|
||||||
fun requestPatientStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
|
fun requestPatientStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
|
||||||
val states = if (delete) {
|
val states = if (delete) {
|
||||||
requestService.findPatientUniqueDeleteStates()
|
requestRepository.findPatientUniqueDeleteStates()
|
||||||
} else {
|
} else {
|
||||||
requestService.findPatientUniqueStates()
|
requestRepository.findPatientUniqueStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
return states.map {
|
return states.map {
|
||||||
|
@ -3,34 +3,17 @@ spring:
|
|||||||
compose:
|
compose:
|
||||||
file: ./dev-compose.yml
|
file: ./dev-compose.yml
|
||||||
|
|
||||||
security:
|
|
||||||
oauth2:
|
|
||||||
client:
|
|
||||||
registration:
|
|
||||||
custom:
|
|
||||||
client-name: App-Dev
|
|
||||||
client-id: app-dev
|
|
||||||
client-secret: very-secret-ae3f7a-5a9f-1190
|
|
||||||
scope:
|
|
||||||
- openid
|
|
||||||
provider:
|
|
||||||
custom:
|
|
||||||
issuer-uri: https://dnpm.dev/auth/realms/intern
|
|
||||||
user-name-attribute: name
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
rest:
|
#rest:
|
||||||
uri: http://localhost:9000/bwhc/etl/api
|
# uri: http://localhost:9000/bwhc/etl/api
|
||||||
#kafka:
|
kafka:
|
||||||
# topic: test
|
topic: test
|
||||||
# response-topic: test_response
|
response-topic: test_response
|
||||||
# servers: localhost:9094
|
servers: localhost:9094
|
||||||
security:
|
#security:
|
||||||
admin-user: admin
|
# admin-user: admin
|
||||||
admin-password: "{noop}very-secret"
|
# admin-password: "{noop}very-secret"
|
||||||
enable-oidc: "true"
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8000
|
port: 8000
|
||||||
|
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE request RENAME COLUMN patient_id TO patient_pseudonym;
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE request RENAME COLUMN patient_id TO patient_pseudonym;
|
|
@ -22,10 +22,6 @@
|
|||||||
--bg-gray-op: rgba(112, 128, 144, .35);
|
--bg-gray-op: rgba(112, 128, 144, .35);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background: linear-gradient(-5deg, var(--bg-blue-op), transparent 10em);
|
background: linear-gradient(-5deg, var(--bg-blue-op), transparent 10em);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@ -34,6 +30,7 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0 0 5em 0;
|
margin: 0 0 5em 0;
|
||||||
|
font-family: sans-serif;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
||||||
@ -622,10 +619,6 @@ input.inline:focus-visible {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification.info {
|
|
||||||
color: var(--bg-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification.success {
|
.notification.success {
|
||||||
color: var(--bg-green);
|
color: var(--bg-green);
|
||||||
}
|
}
|
||||||
@ -650,16 +643,14 @@ input.inline:focus-visible {
|
|||||||
|
|
||||||
.tab:hover,
|
.tab:hover,
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: var(--bg-gray);
|
background: var(--table-border);
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabcontent {
|
.tabcontent {
|
||||||
border: 2px solid var(--bg-gray);
|
border: 1px solid var(--table-border);
|
||||||
border-radius: 0 .5em .5em .5em;
|
border-radius: 0 .5em .5em .5em;
|
||||||
display: none;
|
display: none;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabcontent.active {
|
.tabcontent.active {
|
||||||
@ -697,14 +688,4 @@ a.reload {
|
|||||||
.no-token {
|
.no-token {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background: var(--bg-red-op);
|
background: var(--bg-red-op);
|
||||||
}
|
|
||||||
|
|
||||||
.issue-message {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
.issue-path {
|
|
||||||
font-family: monospace;
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
}
|
@ -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>
|
|
@ -2,20 +2,15 @@
|
|||||||
<h2><span>🟦</span> gPAS nicht konfiguriert - Patienten-IDs werden intern anonymisiert</h2>
|
<h2><span>🟦</span> gPAS nicht konfiguriert - Patienten-IDs werden intern anonymisiert</h2>
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block th:if="${gPasConnectionAvailable != null}">
|
<th:block th:if="${gPasConnectionAvailable != null}">
|
||||||
<h2><span th:if="${gPasConnectionAvailable.available}">✅</span><span th:if="${not(gPasConnectionAvailable.available)}">⚡</span> Verbindung zu gPAS</h2>
|
<h2><span th:if="${gPasConnectionAvailable}">✅</span><span th:if="${not(gPasConnectionAvailable)}">⚡</span> Verbindung zu gPAS</h2>
|
||||||
<div>
|
<div>
|
||||||
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}"></time>
|
Die Verbindung ist aktuell
|
||||||
|
|
<strong th:if="${gPasConnectionAvailable}" style="color: green">verfügbar.</strong>
|
||||||
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}"></time>
|
<strong th:if="${not(gPasConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Die Verbindung ist aktuell</span>
|
|
||||||
<strong th:if="${gPasConnectionAvailable.available}" style="color: green">verfügbar.</strong>
|
|
||||||
<strong th:if="${not(gPasConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="connection-display border">
|
<div class="connection-display border">
|
||||||
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
||||||
<span class="connection" th:classappend="${gPasConnectionAvailable.available ? 'available' : ''}"></span>
|
<span class="connection" th:classappend="${gPasConnectionAvailable ? 'available' : ''}"></span>
|
||||||
<img th:src="@{/server.png}" alt="gPAS" />
|
<img th:src="@{/server.png}" alt="gPAS" />
|
||||||
<span>ETL-Processor</span>
|
<span>ETL-Processor</span>
|
||||||
<span></span>
|
<span></span>
|
||||||
|
@ -1,27 +1,16 @@
|
|||||||
<th:block th:if="${outputConnectionAvailable == null}">
|
<h2><span th:if="${outputConnectionAvailable}">✅</span><span th:if="${not(outputConnectionAvailable)}">⚡</span> MTB-File Verbindung</h2>
|
||||||
<h2><span>🟦</span> Keine Ausgabenkonfiguration</h2>
|
<div>
|
||||||
</th:block>
|
Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
|
||||||
<th:block th:if="${outputConnectionAvailable != null}">
|
<strong th:if="${outputConnectionAvailable}" style="color: green">verfügbar.</strong>
|
||||||
<h2><span th:if="${outputConnectionAvailable.available}">✅</span><span th:if="${not(outputConnectionAvailable.available)}">⚡</span> MTB-File Verbindung</h2>
|
<strong th:if="${not(outputConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
|
||||||
<div>
|
</div>
|
||||||
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(outputConnectionAvailable.timestamp)}"></time>
|
<div class="connection-display border">
|
||||||
|
|
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
||||||
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(outputConnectionAvailable.lastChange)}"></time>
|
<span class="connection" th:classappend="${outputConnectionAvailable ? 'available' : ''}"></span>
|
||||||
</div>
|
<img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
|
||||||
<div>
|
<img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
|
||||||
Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
|
<span>ETL-Processor</span>
|
||||||
<strong th:if="${outputConnectionAvailable.available}" style="color: green">verfügbar.</strong>
|
<span></span>
|
||||||
<strong th:if="${not(outputConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
|
<span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
|
||||||
</div>
|
<span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
|
||||||
<div class="connection-display border">
|
</div>
|
||||||
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
|
||||||
<span class="connection" th:classappend="${outputConnectionAvailable.available ? 'available' : ''}"></span>
|
|
||||||
<img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
|
|
||||||
<img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
|
|
||||||
<span>ETL-Processor</span>
|
|
||||||
<span></span>
|
|
||||||
<span th:if="${mtbFileSender.startsWith('RestBwhc')}">bwHC-Backend</span>
|
|
||||||
<span th:if="${mtbFileSender.startsWith('RestDip')}">DNPM:DIP-Backend</span>
|
|
||||||
<span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
|
|
||||||
</div>
|
|
||||||
</th:block>
|
|
@ -12,30 +12,26 @@
|
|||||||
<h1>Alle Anfragen<a id="reload-notify" class="reload" title="Neue Anfragen" th:href="@{/}">⟳</a></h1>
|
<h1>Alle Anfragen<a id="reload-notify" class="reload" title="Neue Anfragen" th:href="@{/}">⟳</a></h1>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 th:if="${patientPseudonym != null}">
|
<h2 th:if="${patientId != null}">
|
||||||
Betreffend Patienten-Pseudonym <span class="monospace" th:text="${patientPseudonym}">***</span>
|
Betreffend Patienten-Pseudonym <span class="monospace" th:text="${patientId}">***</span>
|
||||||
<a class="btn btn-blue" th:if="${patientPseudonym != null}" th:href="@{/}">Alle anzeigen</a>
|
<a class="btn btn-blue" th:if="${patientId != null}" th:href="@{/}">Alle anzeigen</a>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border" th:if="${requests.totalElements == 0}">
|
<div class="border">
|
||||||
<div class="notification info">Noch keine Anfragen eingegangen</div>
|
<div th:if="${patientId == null}" class="page-control">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border" th:if="${requests.totalElements > 0}">
|
|
||||||
<div th:if="${patientPseudonym == null}" class="page-control">
|
|
||||||
<a id="first-page-link" th:href="@{/(page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">⇤</a><a th:if="${requests.isFirst()}">⇤</a>
|
<a id="first-page-link" th:href="@{/(page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">⇤</a><a th:if="${requests.isFirst()}">⇤</a>
|
||||||
<a id="prev-page-link" th:href="@{/(page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">←</a><a th:if="${requests.isFirst()}">←</a>
|
<a id="prev-page-link" th:href="@{/(page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">←</a><a th:if="${requests.isFirst()}">←</a>
|
||||||
<span>Seite [[ ${requests.getNumber() + 1} ]] von [[ ${requests.getTotalPages()} ]]</span>
|
<span>Seite [[ ${requests.getNumber() + 1} ]] von [[ ${requests.getTotalPages()} ]]</span>
|
||||||
<a id="next-page-link" th:href="@{/(page=${requests.getNumber() + 1})}" title="Seite vor: Taste D" th:if="${not requests.isLast()}">→</a><a th:if="${requests.isLast()}">→</a>
|
<a id="next-page-link" th:href="@{/(page=${requests.getNumber() + 1})}" title="Seite vor: Taste D" th:if="${not requests.isLast()}">→</a><a th:if="${requests.isLast()}">→</a>
|
||||||
<a id="last-page-link" th:href="@{/(page=${requests.getTotalPages() - 1})}" title="Zum Ende: Taste S" th:if="${not requests.isLast()}">⇥</a><a th:if="${requests.isLast()}">⇥</a>
|
<a id="last-page-link" th:href="@{/(page=${requests.getTotalPages() - 1})}" title="Zum Ende: Taste S" th:if="${not requests.isLast()}">⇥</a><a th:if="${requests.isLast()}">⇥</a>
|
||||||
</div>
|
</div>
|
||||||
<div th:if="${patientPseudonym != null}" class="page-control">
|
<div th:if="${patientId != null}" class="page-control">
|
||||||
<a id="first-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">⇤</a><a th:if="${requests.isFirst()}">⇤</a>
|
<a id="first-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">⇤</a><a th:if="${requests.isFirst()}">⇤</a>
|
||||||
<a id="prev-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">←</a><a th:if="${requests.isFirst()}">←</a>
|
<a id="prev-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">←</a><a th:if="${requests.isFirst()}">←</a>
|
||||||
<span>Seite [[ ${requests.getNumber() + 1} ]] von [[ ${requests.getTotalPages()} ]]</span>
|
<span>Seite [[ ${requests.getNumber() + 1} ]] von [[ ${requests.getTotalPages()} ]]</span>
|
||||||
<a id="next-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${requests.getNumber() + 1})}" title="Seite vor: Taste D" th:if="${not requests.isLast()}">→</a><a th:if="${requests.isLast()}">→</a>
|
<a id="next-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${requests.getNumber() + 1})}" title="Seite vor: Taste D" th:if="${not requests.isLast()}">→</a><a th:if="${requests.isLast()}">→</a>
|
||||||
<a id="last-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${requests.getTotalPages() - 1})}" title="Zum Ende: Taste S" th:if="${not requests.isLast()}">⇥</a><a th:if="${requests.isLast()}">⇥</a>
|
<a id="last-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${requests.getTotalPages() - 1})}" title="Zum Ende: Taste S" th:if="${not requests.isLast()}">⇥</a><a th:if="${requests.isLast()}">⇥</a>
|
||||||
</div>
|
</div>
|
||||||
<table class="paged">
|
<table class="paged">
|
||||||
<thead>
|
<thead>
|
||||||
@ -61,11 +57,11 @@
|
|||||||
<th:block sec:authorize="not (hasRole('USER') or hasRole('ADMIN'))">[[ ${request.uuid} ]]</th:block>
|
<th:block sec:authorize="not (hasRole('USER') or hasRole('ADMIN'))">[[ ${request.uuid} ]]</th:block>
|
||||||
</td>
|
</td>
|
||||||
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td>
|
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td>
|
||||||
<td class="patient-id" th:if="${patientPseudonym != null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
|
<td class="patient-id" th:if="${patientId != null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
|
||||||
[[ ${request.patientPseudonym} ]]
|
[[ ${request.patientId} ]]
|
||||||
</td>
|
</td>
|
||||||
<td class="patient-id" th:if="${patientPseudonym == null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
|
<td class="patient-id" th:if="${patientId == null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
|
||||||
<a th:href="@{/patient/{pid}(pid=${request.patientPseudonym})}">[[ ${request.patientPseudonym} ]]</a>
|
<a th:href="@{/patient/{pid}(pid=${request.patientId})}">[[ ${request.patientId} ]]</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="patient-id" sec:authorize="not (hasRole('USER') or hasRole('ADMIN'))">***</td>
|
<td class="patient-id" sec:authorize="not (hasRole('USER') or hasRole('ADMIN'))">***</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>
|
<td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>
|
||||||
<td>[[ ${request.uuid} ]]</td>
|
<td>[[ ${request.uuid} ]]</td>
|
||||||
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td>
|
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td>
|
||||||
<td class="patient-id" sec:authorize="authenticated">[[ ${request.patientPseudonym} ]]</td>
|
<td class="patient-id" sec:authorize="authenticated">[[ ${request.patientId} ]]</td>
|
||||||
<td class="patient-id" sec:authorize="not authenticated">***</td>
|
<td class="patient-id" sec:authorize="not authenticated">***</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -47,7 +47,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Schweregrad</th>
|
<th>Schweregrad</th>
|
||||||
<th>Beschreibung und Pfad</th>
|
<th>Beschreibung</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -56,11 +56,7 @@
|
|||||||
<td th:if="${issue.severity.value == 'warning'}" class="bg-yellow"><small>[[ ${issue.severity} ]]</small></td>
|
<td th:if="${issue.severity.value == 'warning'}" class="bg-yellow"><small>[[ ${issue.severity} ]]</small></td>
|
||||||
<td th:if="${issue.severity.value == 'error'}" class="bg-red"><small>[[ ${issue.severity} ]]</small></td>
|
<td th:if="${issue.severity.value == 'error'}" class="bg-red"><small>[[ ${issue.severity} ]]</small></td>
|
||||||
<td th:if="${issue.severity.value == 'fatal'}" class="bg-red"><small>[[ ${issue.severity} ]]</small></td>
|
<td th:if="${issue.severity.value == 'fatal'}" class="bg-red"><small>[[ ${issue.severity} ]]</small></td>
|
||||||
<td>
|
<td>[[ ${issue.message} ]]</td>
|
||||||
<div class="issue-message">[[ ${issue.message} ]]</div>
|
|
||||||
<div class="issue-path" th:if="${issue.path.isPresent()}">[[ ${issue.path.get()} ]]</div>
|
|
||||||
<div class="issue-path" th:if="${issue.path.isEmpty()}"><i>Keine Angabe</i></div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -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,20 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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
|
|
@ -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
|
||||||
@ -33,9 +31,12 @@ import org.apache.kafka.common.record.TimestampType
|
|||||||
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.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.any
|
||||||
|
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(anyString())
|
||||||
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(), anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@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(), eq(
|
verify(requestProcessor, times(1)).processDeletion(anyString(), anyString())
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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,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.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.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.argumentCaptor
|
||||||
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
|
||||||
@ -52,268 +41,24 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders
|
|||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class MtbFileRestControllerTest {
|
class MtbFileRestControllerTest {
|
||||||
|
|
||||||
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
|
private lateinit var requestProcessor: RequestProcessor
|
||||||
|
|
||||||
private val objectMapper = ObjectMapper()
|
private val objectMapper = ObjectMapper()
|
||||||
|
|
||||||
@Nested
|
@BeforeEach
|
||||||
inner class BwhcRequests {
|
fun setup(
|
||||||
|
@Mock requestProcessor: RequestProcessor
|
||||||
private lateinit var mockMvc: MockMvc
|
) {
|
||||||
|
this.requestProcessor = requestProcessor
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
val controller = MtbFileRestController(requestProcessor)
|
||||||
|
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock requestProcessor: RequestProcessor
|
|
||||||
) {
|
|
||||||
this.requestProcessor = requestProcessor
|
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
|
||||||
ConsentCheckFileBased()
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessPostRequest() {
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
|
||||||
mockMvc.post("/mtbfile") {
|
|
||||||
content =
|
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestPropertySource(
|
@Test
|
||||||
properties = ["app.consent.gics.enabled=true",
|
fun shouldProcessMtbFilePostRequest() {
|
||||||
"app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"]
|
val mtbFile = MtbFile.builder()
|
||||||
)
|
|
||||||
@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
|
|
||||||
inner class BwhcRequestsWithAlias {
|
|
||||||
|
|
||||||
private lateinit var mockMvc: MockMvc
|
|
||||||
|
|
||||||
private lateinit var requestProcessor: RequestProcessor
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock requestProcessor: RequestProcessor
|
|
||||||
) {
|
|
||||||
this.requestProcessor = requestProcessor
|
|
||||||
val controller = MtbFileRestController(requestProcessor,
|
|
||||||
ConsentCheckFileBased()
|
|
||||||
)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessPostRequest() {
|
|
||||||
mockMvc.post("/mtb") {
|
|
||||||
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessPostRequestWithRejectedConsent() {
|
|
||||||
mockMvc.post("/mtb") {
|
|
||||||
content =
|
|
||||||
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
|
|
||||||
contentType = MediaType.APPLICATION_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
|
||||||
anyValueClass(), org.mockito.kotlin.eq(
|
|
||||||
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldProcessDeleteRequest() {
|
|
||||||
mockMvc.delete("/mtb/TEST_12345678").andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processDeletion(
|
|
||||||
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 {
|
|
||||||
fun bwhcMtbFileContent(consentStatus: Status) = MtbFile.builder()
|
|
||||||
.withPatient(
|
.withPatient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("TEST_12345678")
|
.withId("TEST_12345678")
|
||||||
@ -324,7 +69,7 @@ class MtbFileRestControllerTest {
|
|||||||
.withConsent(
|
.withConsent(
|
||||||
Consent.builder()
|
Consent.builder()
|
||||||
.withId("1")
|
.withId("1")
|
||||||
.withStatus(consentStatus)
|
.withStatus(Consent.Status.ACTIVE)
|
||||||
.withPatient("TEST_12345678")
|
.withPatient("TEST_12345678")
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@ -336,5 +81,70 @@ class MtbFileRestControllerTest {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
mockMvc.post("/mtbfile") {
|
||||||
|
content = objectMapper.writeValueAsString(mtbFile)
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
}.andExpect {
|
||||||
|
status {
|
||||||
|
isAccepted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Test
|
||||||
|
fun shouldProcessMtbFilePostRequestWithRejectedConsent() {
|
||||||
|
val mtbFile = MtbFile.builder()
|
||||||
|
.withPatient(
|
||||||
|
Patient.builder()
|
||||||
|
.withId("TEST_12345678")
|
||||||
|
.withBirthDate("2000-08-08")
|
||||||
|
.withGender(Patient.Gender.MALE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withConsent(
|
||||||
|
Consent.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withStatus(Consent.Status.REJECTED)
|
||||||
|
.withPatient("TEST_12345678")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withEpisode(
|
||||||
|
Episode.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withPatient("TEST_12345678")
|
||||||
|
.withPeriod(PeriodStart("2023-08-08"))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
mockMvc.post("/mtbfile") {
|
||||||
|
content = objectMapper.writeValueAsString(mtbFile)
|
||||||
|
contentType = MediaType.APPLICATION_JSON
|
||||||
|
}.andExpect {
|
||||||
|
status {
|
||||||
|
isAccepted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val captor = argumentCaptor<String>()
|
||||||
|
verify(requestProcessor, times(1)).processDeletion(captor.capture())
|
||||||
|
assertThat(captor.firstValue).isEqualTo("TEST_12345678")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldProcessMtbFileDeleteRequest() {
|
||||||
|
mockMvc.delete("/mtbfile/TEST_12345678").andExpect {
|
||||||
|
status {
|
||||||
|
isAccepted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val captor = argumentCaptor<String>()
|
||||||
|
verify(requestProcessor, times(1)).processDeletion(captor.capture())
|
||||||
|
assertThat(captor.firstValue).isEqualTo("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,262 +21,161 @@ 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.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
|
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
||||||
inner class BwhcV1Record {
|
|
||||||
|
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
||||||
|
|
||||||
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
private lateinit var objectMapper: ObjectMapper
|
||||||
|
|
||||||
private lateinit var objectMapper: ObjectMapper
|
@BeforeEach
|
||||||
|
fun setup(
|
||||||
|
@Mock kafkaTemplate: KafkaTemplate<String, String>
|
||||||
|
) {
|
||||||
|
val kafkaProperties = KafkaProperties("testtopic")
|
||||||
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
@BeforeEach
|
this.objectMapper = ObjectMapper()
|
||||||
fun setup(
|
this.kafkaTemplate = kafkaTemplate
|
||||||
@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(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
|
||||||
assertThat(response.status).isEqualTo(testData.requestStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
|
||||||
fun shouldSendDeleteRequestAndReturnExpectedState(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(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
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(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
|
||||||
|
|
||||||
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(MediaType.APPLICATION_JSON_VALUE.toByteArray())
|
|
||||||
assertThat(captor.firstValue.value()).isNotNull
|
|
||||||
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(bwhcV1kafkaRecordData(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
|
||||||
doAnswer {
|
|
||||||
completedFuture(SendResult<String, String>(null, null))
|
|
||||||
}.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>())
|
|
||||||
|
|
||||||
kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
|
|
||||||
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.value()).isNotNull
|
|
||||||
assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(bwhcV1kafkaRecordData(TEST_REQUEST_ID, Consent.Status.REJECTED)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@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(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1MtbFile(Consent.Status.ACTIVE)))
|
|
||||||
|
|
||||||
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>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource")
|
|
||||||
fun shouldRetryOnDeleteKafkaSendError(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(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
|
|
||||||
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>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@ParameterizedTest
|
||||||
inner class DnpmV2Record {
|
@MethodSource("requestWithResponseSource")
|
||||||
|
fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) {
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, String>
|
doAnswer {
|
||||||
|
if (null != testData.exception) {
|
||||||
private lateinit var kafkaMtbFileSender: KafkaMtbFileSender
|
throw testData.exception
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>())
|
val response = kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile(Consent.Status.ACTIVE)))
|
||||||
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("requestWithResponseSource")
|
||||||
|
fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) {
|
||||||
|
doAnswer {
|
||||||
|
if (null != testData.exception) {
|
||||||
|
throw testData.exception
|
||||||
|
}
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
|
val response = kafkaMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
|
||||||
|
assertThat(response.status).isEqualTo(testData.requestStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldSendMtbFileRequestWithCorrectKeyAndBody() {
|
||||||
|
doAnswer {
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile(Consent.Status.ACTIVE)))
|
||||||
|
|
||||||
|
val captor = argumentCaptor<String>()
|
||||||
|
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
||||||
|
assertThat(captor.firstValue).isNotNull
|
||||||
|
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
|
assertThat(captor.secondValue).isNotNull
|
||||||
|
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData("TestID", Consent.Status.ACTIVE)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
||||||
|
doAnswer {
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
|
||||||
|
|
||||||
|
val captor = argumentCaptor<String>()
|
||||||
|
verify(kafkaTemplate, times(1)).send(anyString(), captor.capture(), captor.capture())
|
||||||
|
assertThat(captor.firstValue).isNotNull
|
||||||
|
assertThat(captor.firstValue).isEqualTo("{\"pid\": \"PID\"}")
|
||||||
|
assertThat(captor.secondValue).isNotNull
|
||||||
|
assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData("TestID", Consent.Status.REJECTED)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("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(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile(Consent.Status.ACTIVE)))
|
||||||
|
|
||||||
|
val expectedCount = when (testData.exception) {
|
||||||
|
// OK - No Retry
|
||||||
|
null -> times(1)
|
||||||
|
// Request failed - Retry max 3 times
|
||||||
|
else -> times(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("requestWithResponseSource")
|
||||||
|
fun shouldRetryOnDeleteKafkaSendError(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(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
|
||||||
|
|
||||||
|
val expectedCount = when (testData.exception) {
|
||||||
|
// OK - No Retry
|
||||||
|
null -> times(1)
|
||||||
|
// Request failed - Retry max 3 times
|
||||||
|
else -> times(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(kafkaTemplate, expectedCount).send(anyString(), anyString(), anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
fun mtbFile(consentStatus: Consent.Status): MtbFile {
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
|
||||||
|
|
||||||
fun bwhcV1MtbFile(consentStatus: Consent.Status): MtbFile {
|
|
||||||
return if (consentStatus == Consent.Status.ACTIVE) {
|
return if (consentStatus == Consent.Status.ACTIVE) {
|
||||||
MtbFile.builder()
|
MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -311,35 +210,8 @@ class KafkaMtbFileSenderTest {
|
|||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dnpmV2MtbFile(): Mtb {
|
fun kafkaRecordData(requestId: String, 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)
|
||||||
@ -354,4 +226,4 @@ class KafkaMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,411 +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 com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|
||||||
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.RequestId
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
|
||||||
import org.springframework.http.HttpHeaders
|
|
||||||
import org.springframework.http.HttpMethod
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.retry.backoff.NoBackOffPolicy
|
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
|
||||||
import org.springframework.web.client.RestTemplate
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class RestDipMtbFileSenderTest {
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class BwhcV1ContentRequest {
|
|
||||||
|
|
||||||
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 shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.POST))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
|
||||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1mtbFile))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = AppConfiguration().retryTemplate(AppConfigProperties("http://localhost:9000"))
|
|
||||||
retryTemplate.setBackOffPolicy(NoBackOffPolicy())
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
|
||||||
// OK - No Retry
|
|
||||||
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(
|
|
||||||
1
|
|
||||||
)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> ExpectedCount.max(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(expectedCount, 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(BwhcV1MtbFileRequest(TEST_REQUEST_ID, bwhcV1mtbFile))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class DnpmV2ContentRequest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
|
||||||
|
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
|
|
||||||
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForDnpmV2MtbFilePost(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.POST))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
|
||||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile()))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class DeleteRequest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
|
||||||
|
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
|
||||||
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(method(HttpMethod.DELETE))
|
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
|
||||||
.andRespond {
|
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource")
|
|
||||||
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
|
||||||
val restTemplate = RestTemplate()
|
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null, false)
|
|
||||||
val retryTemplate = AppConfiguration().retryTemplate(AppConfigProperties("http://localhost:9000"))
|
|
||||||
retryTemplate.setBackOffPolicy(NoBackOffPolicy())
|
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
|
||||||
this.restMtbFileSender =
|
|
||||||
RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
|
||||||
// OK - No Retry
|
|
||||||
HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max(
|
|
||||||
1
|
|
||||||
)
|
|
||||||
// Request failed - Retry max 3 times
|
|
||||||
else -> ExpectedCount.max(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mockRestServiceServer
|
|
||||||
.expect(expectedCount, 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.body).isEqualTo(requestWithResponse.response.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
data class RequestWithResponse(
|
|
||||||
val httpStatus: HttpStatus,
|
|
||||||
val body: String,
|
|
||||||
val response: MtbFileSender.Response
|
|
||||||
)
|
|
||||||
|
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
|
||||||
|
|
||||||
val bwhcV1mtbFile: MtbFile = MtbFile.builder()
|
|
||||||
.withPatient(
|
|
||||||
Patient.builder()
|
|
||||||
.withId("PID")
|
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withConsent(
|
|
||||||
Consent.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
|
||||||
.withPatient("PID")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withEpisode(
|
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("PID")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.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"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic http responses with related request status
|
|
||||||
* Also see: https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> {
|
|
||||||
return setOf(
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.OK,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.INFO),
|
|
||||||
MtbFileSender.Response(
|
|
||||||
RequestStatus.SUCCESS,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.INFO)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.CREATED,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.WARNING),
|
|
||||||
MtbFileSender.Response(RequestStatus.WARNING, responseBodyWithMaxSeverity(ReportService.Severity.WARNING))
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.ERROR),
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR))
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.ERROR),
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR))
|
|
||||||
),
|
|
||||||
// Some more errors not mentioned in documentation
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
ERROR_RESPONSE_BODY,
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
ERROR_RESPONSE_BODY,
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthetic http responses with related request status
|
|
||||||
* Also see: https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun deleteRequestWithResponseSource(): Set<RequestWithResponse> {
|
|
||||||
return setOf(
|
|
||||||
RequestWithResponse(HttpStatus.OK, "", MtbFileSender.Response(RequestStatus.SUCCESS)),
|
|
||||||
// Some more errors not mentioned in documentation
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
"what????",
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"what????",
|
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun responseBodyWithMaxSeverity(severity: ReportService.Severity): String {
|
|
||||||
return when (severity) {
|
|
||||||
ReportService.Severity.INFO -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.WARNING -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.ERROR -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.FATAL -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" },
|
|
||||||
{ "severity": "fatal", "message": "Fatal Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -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,61 +19,52 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.RequestId
|
|
||||||
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 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.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
|
||||||
|
|
||||||
class RestBwhcMtbFileSenderTest {
|
class RestMtbFileSenderTest {
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
|
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val restTemplate = RestTemplate()
|
val restTemplate = RestTemplate()
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile")
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
|
|
||||||
this.restMtbFileSender =
|
this.restMtbFileSender = RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
|
||||||
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("deleteRequestWithResponseSource")
|
@MethodSource("deleteRequestWithResponseSource")
|
||||||
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
||||||
this.mockRestServiceServer
|
this.mockRestServiceServer.expect {
|
||||||
.expect(method(HttpMethod.DELETE))
|
method(HttpMethod.DELETE)
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/Patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
requestTo("/mtbfile")
|
||||||
.andRespond {
|
}.andRespond {
|
||||||
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("TestID", "PID"))
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -81,15 +72,14 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
@MethodSource("mtbFileRequestWithResponseSource")
|
||||||
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
||||||
this.mockRestServiceServer
|
this.mockRestServiceServer.expect {
|
||||||
.expect(method(HttpMethod.POST))
|
method(HttpMethod.POST)
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
requestTo("/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("TestID", 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)
|
||||||
}
|
}
|
||||||
@ -98,12 +88,11 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
@MethodSource("mtbFileRequestWithResponseSource")
|
@MethodSource("mtbFileRequestWithResponseSource")
|
||||||
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
val restTemplate = RestTemplate()
|
val restTemplate = RestTemplate()
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile")
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
this.restMtbFileSender =
|
this.restMtbFileSender = RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
|
||||||
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
val expectedCount = when (requestWithResponse.httpStatus) {
|
||||||
// OK - No Retry
|
// OK - No Retry
|
||||||
@ -112,14 +101,14 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
else -> ExpectedCount.max(3)
|
else -> ExpectedCount.max(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mockRestServiceServer
|
this.mockRestServiceServer.expect(expectedCount) {
|
||||||
.expect(expectedCount, method(HttpMethod.POST))
|
method(HttpMethod.POST)
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
requestTo("/mtbfile")
|
||||||
.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("TestID", 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)
|
||||||
}
|
}
|
||||||
@ -128,12 +117,11 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
@MethodSource("deleteRequestWithResponseSource")
|
@MethodSource("deleteRequestWithResponseSource")
|
||||||
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
val restTemplate = RestTemplate()
|
val restTemplate = RestTemplate()
|
||||||
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
|
val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile")
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
|
||||||
|
|
||||||
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
this.restMtbFileSender =
|
this.restMtbFileSender = RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate)
|
||||||
RestBwhcMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
|
||||||
|
|
||||||
val expectedCount = when (requestWithResponse.httpStatus) {
|
val expectedCount = when (requestWithResponse.httpStatus) {
|
||||||
// OK - No Retry
|
// OK - No Retry
|
||||||
@ -142,14 +130,14 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
else -> ExpectedCount.max(3)
|
else -> ExpectedCount.max(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mockRestServiceServer
|
this.mockRestServiceServer.expect(expectedCount) {
|
||||||
.expect(expectedCount, method(HttpMethod.DELETE))
|
method(HttpMethod.DELETE)
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/Patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
requestTo("/mtbfile")
|
||||||
.andRespond {
|
}.andRespond {
|
||||||
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("TestID", "PID"))
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -161,8 +149,23 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
val response: MtbFileSender.Response
|
val response: MtbFileSender.Response
|
||||||
)
|
)
|
||||||
|
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
private val warningBody = """
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
{
|
||||||
|
"patient_id": "PID",
|
||||||
|
"issues": [
|
||||||
|
{ "severity": "warning", "message": "Something is not right" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
private val errorBody = """
|
||||||
|
{
|
||||||
|
"patient_id": "PID",
|
||||||
|
"issues": [
|
||||||
|
{ "severity": "error", "message": "Something is very bad" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
val mtbFile: MtbFile = MtbFile.builder()
|
val mtbFile: MtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -197,44 +200,31 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> {
|
fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> {
|
||||||
return setOf(
|
return setOf(
|
||||||
RequestWithResponse(
|
RequestWithResponse(HttpStatus.OK, "{}", MtbFileSender.Response(RequestStatus.SUCCESS, "{}")),
|
||||||
HttpStatus.OK,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.INFO),
|
|
||||||
MtbFileSender.Response(
|
|
||||||
RequestStatus.SUCCESS,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.INFO)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
RequestWithResponse(
|
RequestWithResponse(
|
||||||
HttpStatus.CREATED,
|
HttpStatus.CREATED,
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.WARNING),
|
warningBody,
|
||||||
MtbFileSender.Response(
|
MtbFileSender.Response(RequestStatus.WARNING, warningBody)
|
||||||
RequestStatus.WARNING,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.WARNING)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
RequestWithResponse(
|
RequestWithResponse(
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.ERROR),
|
"??",
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR))
|
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
||||||
),
|
),
|
||||||
RequestWithResponse(
|
RequestWithResponse(
|
||||||
HttpStatus.UNPROCESSABLE_ENTITY,
|
HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.FATAL),
|
errorBody,
|
||||||
MtbFileSender.Response(
|
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
||||||
RequestStatus.ERROR,
|
|
||||||
responseBodyWithMaxSeverity(ReportService.Severity.FATAL)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
// Some more errors not mentioned in documentation
|
// Some more errors not mentioned in documentation
|
||||||
RequestWithResponse(
|
RequestWithResponse(
|
||||||
HttpStatus.NOT_FOUND,
|
HttpStatus.NOT_FOUND,
|
||||||
ERROR_RESPONSE_BODY,
|
"what????",
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
||||||
),
|
),
|
||||||
RequestWithResponse(
|
RequestWithResponse(
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
ERROR_RESPONSE_BODY,
|
"what????",
|
||||||
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -261,53 +251,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun responseBodyWithMaxSeverity(severity: ReportService.Severity): String {
|
|
||||||
return when (severity) {
|
|
||||||
ReportService.Severity.INFO -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.WARNING -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.ERROR -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
ReportService.Severity.FATAL -> """
|
|
||||||
{
|
|
||||||
"patient": "PID",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" },
|
|
||||||
{ "severity": "fatal", "message": "Fatal Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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,260 +21,178 @@ 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
|
||||||
|
import org.mockito.ArgumentMatchers
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
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()
|
private fun fakeMtbFile(): MtbFile {
|
||||||
|
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
||||||
|
return ObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
private fun MtbFile.serialized(): String {
|
||||||
inner class UsingBwhcDatamodel {
|
return ObjectMapper().writeValueAsString(this)
|
||||||
|
|
||||||
val FAKE_MTB_FILE_PATH = "fake_MTBFile.json"
|
|
||||||
val CLEAN_PATIENT_ID = "5dad2f0b-49c6-47d8-a952-7b9e9e0f7549"
|
|
||||||
|
|
||||||
|
|
||||||
private fun fakeMtbFile(): MtbFile {
|
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
|
||||||
return getObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MtbFile.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 shouldNotContainAnyUuidAfterRehashingOfIds(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = fakeMtbFile()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
val pattern = "\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex().toPattern()
|
|
||||||
val matcher = pattern.matcher(mtbFile.serialized())
|
|
||||||
|
|
||||||
assertThrows<IllegalStateException> {
|
|
||||||
matcher.find()
|
|
||||||
matcher.group()
|
|
||||||
}.also {
|
|
||||||
assertThat(it.message).isEqualTo("No match found")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldRehashIdsWithPrefix(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
|
||||||
.withPatient(
|
|
||||||
Patient.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withConsent(
|
|
||||||
Consent.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
|
||||||
.withPatient("123")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withEpisode(
|
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
|
|
||||||
assertThat(mtbFile.episode.id)
|
|
||||||
// TESTDOMAIN<sha256(TESTDOMAIN-1)[0-41]>
|
|
||||||
.isEqualTo("TESTDOMAIN44e20a53bbbf9f3ae39626d05df7014dcd77d6098")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
|
||||||
.withPatient(
|
|
||||||
Patient.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withBirthDate("2000-08-08")
|
|
||||||
.withGender(Patient.Gender.MALE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withConsent(
|
|
||||||
Consent.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withStatus(Consent.Status.ACTIVE)
|
|
||||||
.withPatient("123")
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withEpisode(
|
|
||||||
Episode.builder()
|
|
||||||
.withId("1")
|
|
||||||
.withPatient("1")
|
|
||||||
.withPeriod(PeriodStart("2023-08-08"))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.withClaims(null)
|
|
||||||
.withDiagnoses(null)
|
|
||||||
.withCarePlans(null)
|
|
||||||
.withClaimResponses(null)
|
|
||||||
.withEcogStatus(null)
|
|
||||||
.withFamilyMemberDiagnoses(null)
|
|
||||||
.withGeneticCounsellingRequests(null)
|
|
||||||
.withHistologyReevaluationRequests(null)
|
|
||||||
.withHistologyReports(null)
|
|
||||||
.withLastGuidelineTherapies(null)
|
|
||||||
.withMolecularPathologyFindings(null)
|
|
||||||
.withMolecularTherapies(null)
|
|
||||||
.withNgsReports(null)
|
|
||||||
.withPreviousGuidelineTherapies(null)
|
|
||||||
.withRebiopsyRequests(null)
|
|
||||||
.withRecommendations(null)
|
|
||||||
.withResponses(null)
|
|
||||||
.withStudyInclusionRequests(null)
|
|
||||||
.withSpecimens(null)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
assertThat(mtbFile.episode.id).isNotNull()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Test
|
||||||
inner class UsingDnpmV2Datamodel {
|
fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
|
doAnswer {
|
||||||
|
it.arguments[0]
|
||||||
|
"PSEUDO-ID"
|
||||||
|
}.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
|
||||||
|
|
||||||
val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json"
|
val mtbFile = fakeMtbFile()
|
||||||
val CLEAN_PATIENT_ID = "e14bf9b6-7982-4933-a648-cfdea6484f1c"
|
|
||||||
|
|
||||||
private fun fakeMtbFile(): Mtb {
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
|
||||||
return getObjectMapper().readValue(mtbFile, Mtb::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Mtb.serialized(): String {
|
assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID")
|
||||||
return getObjectMapper().writeValueAsString(this)
|
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Test
|
||||||
|
fun shouldNotContainAnyUuidAfterRehashingOfIds(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
|
doAnswer {
|
||||||
|
it.arguments[0]
|
||||||
|
"PSEUDO-ID"
|
||||||
|
}.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
"TESTDOMAIN"
|
||||||
|
}.whenever(pseudonymizeService).prefix()
|
||||||
|
|
||||||
|
val mtbFile = fakeMtbFile()
|
||||||
|
|
||||||
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
|
mtbFile.anonymizeContentWith(pseudonymizeService)
|
||||||
|
|
||||||
|
val pattern = "\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex().toPattern()
|
||||||
|
val matcher = pattern.matcher(mtbFile.serialized())
|
||||||
|
|
||||||
|
assertThrows<IllegalStateException> {
|
||||||
|
matcher.find()
|
||||||
|
matcher.group()
|
||||||
|
}.also {
|
||||||
|
assertThat(it.message).isEqualTo("No match found")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldRehashIdsWithPrefix(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
|
doAnswer {
|
||||||
|
it.arguments[0]
|
||||||
|
"PSEUDO-ID"
|
||||||
|
}.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
"TESTDOMAIN"
|
||||||
|
}.whenever(pseudonymizeService).prefix()
|
||||||
|
|
||||||
|
val mtbFile = MtbFile.builder()
|
||||||
|
.withPatient(
|
||||||
|
Patient.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withBirthDate("2000-08-08")
|
||||||
|
.withGender(Patient.Gender.MALE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withConsent(
|
||||||
|
Consent.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withStatus(Consent.Status.ACTIVE)
|
||||||
|
.withPatient("123")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withEpisode(
|
||||||
|
Episode.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withPatient("1")
|
||||||
|
.withPeriod(PeriodStart("2023-08-08"))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
|
mtbFile.anonymizeContentWith(pseudonymizeService)
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(mtbFile.episode.id)
|
||||||
|
// TESTDOMAIN<sha256(TESTDOMAIN-1)[0-41]>
|
||||||
|
.isEqualTo("TESTDOMAIN44e20a53bbbf9f3ae39626d05df7014dcd77d6098")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
||||||
|
doAnswer {
|
||||||
|
it.arguments[0]
|
||||||
|
"PSEUDO-ID"
|
||||||
|
}.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
|
||||||
|
|
||||||
|
doAnswer {
|
||||||
|
"TESTDOMAIN"
|
||||||
|
}.whenever(pseudonymizeService).prefix()
|
||||||
|
|
||||||
|
val mtbFile = MtbFile.builder()
|
||||||
|
.withPatient(
|
||||||
|
Patient.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withBirthDate("2000-08-08")
|
||||||
|
.withGender(Patient.Gender.MALE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withConsent(
|
||||||
|
Consent.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withStatus(Consent.Status.ACTIVE)
|
||||||
|
.withPatient("123")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withEpisode(
|
||||||
|
Episode.builder()
|
||||||
|
.withId("1")
|
||||||
|
.withPatient("1")
|
||||||
|
.withPeriod(PeriodStart("2023-08-08"))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withClaims(null)
|
||||||
|
.withDiagnoses(null)
|
||||||
|
.withCarePlans(null)
|
||||||
|
.withClaimResponses(null)
|
||||||
|
.withEcogStatus(null)
|
||||||
|
.withFamilyMemberDiagnoses(null)
|
||||||
|
.withGeneticCounsellingRequests(null)
|
||||||
|
.withHistologyReevaluationRequests(null)
|
||||||
|
.withHistologyReports(null)
|
||||||
|
.withLastGuidelineTherapies(null)
|
||||||
|
.withMolecularPathologyFindings(null)
|
||||||
|
.withMolecularTherapies(null)
|
||||||
|
.withNgsReports(null)
|
||||||
|
.withPreviousGuidelineTherapies(null)
|
||||||
|
.withRebiopsyRequests(null)
|
||||||
|
.withRecommendations(null)
|
||||||
|
.withResponses(null)
|
||||||
|
.withStudyInclusionRequests(null)
|
||||||
|
.withSpecimens(null)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
mtbFile.pseudonymizeWith(pseudonymizeService)
|
||||||
|
mtbFile.anonymizeContentWith(pseudonymizeService)
|
||||||
|
|
||||||
|
|
||||||
|
assertThat(mtbFile.episode.id).isNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,202 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ETL-Processor
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* 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.security
|
|
||||||
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
|
||||||
import org.mockito.Mock
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
|
||||||
import org.mockito.kotlin.*
|
|
||||||
import org.springframework.security.core.session.SessionInformation
|
|
||||||
import org.springframework.security.core.session.SessionRegistry
|
|
||||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken
|
|
||||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser
|
|
||||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
|
||||||
class UserRoleServiceTest {
|
|
||||||
|
|
||||||
private lateinit var userRoleRepository: UserRoleRepository
|
|
||||||
private lateinit var sessionRegistry: SessionRegistry
|
|
||||||
|
|
||||||
private lateinit var userRoleService: UserRoleService
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup(
|
|
||||||
@Mock userRoleRepository: UserRoleRepository,
|
|
||||||
@Mock sessionRegistry: SessionRegistry
|
|
||||||
) {
|
|
||||||
this.userRoleRepository = userRoleRepository
|
|
||||||
this.sessionRegistry = sessionRegistry
|
|
||||||
|
|
||||||
this.userRoleService = UserRoleService(userRoleRepository, sessionRegistry)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldDelegateFindAllToRepository() {
|
|
||||||
userRoleService.findAll()
|
|
||||||
|
|
||||||
verify(userRoleRepository, times(1)).findAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class WithExistingUserRole {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
doAnswer { invocation ->
|
|
||||||
Optional.of(
|
|
||||||
UserRole(invocation.getArgument(0), "patrick.tester", Role.USER)
|
|
||||||
)
|
|
||||||
}.whenever(userRoleRepository).findById(any<Long>())
|
|
||||||
|
|
||||||
doAnswer { _ ->
|
|
||||||
listOf(
|
|
||||||
dummyPrincipal()
|
|
||||||
)
|
|
||||||
}.whenever(sessionRegistry).allPrincipals
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldUpdateUserRole() {
|
|
||||||
userRoleService.updateUserRole(1, Role.ADMIN)
|
|
||||||
|
|
||||||
val userRoleCaptor = argumentCaptor<UserRole>()
|
|
||||||
verify(userRoleRepository, times(1)).save(userRoleCaptor.capture())
|
|
||||||
|
|
||||||
assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L)
|
|
||||||
assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.ADMIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldExpireSessionOnUpdate() {
|
|
||||||
val dummySessions = dummySessions()
|
|
||||||
whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn(
|
|
||||||
dummySessions
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(dummySessions.filter { it.isExpired }).hasSize(0)
|
|
||||||
|
|
||||||
userRoleService.updateUserRole(1, Role.ADMIN)
|
|
||||||
|
|
||||||
verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>())
|
|
||||||
|
|
||||||
assertThat(dummySessions.filter { it.isExpired }).hasSize(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldDeleteUserRole() {
|
|
||||||
userRoleService.deleteUserRole(1)
|
|
||||||
|
|
||||||
val userRoleCaptor = argumentCaptor<UserRole>()
|
|
||||||
verify(userRoleRepository, times(1)).delete(userRoleCaptor.capture())
|
|
||||||
|
|
||||||
assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L)
|
|
||||||
assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.USER)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldExpireSessionOnDelete() {
|
|
||||||
val dummySessions = dummySessions()
|
|
||||||
whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn(
|
|
||||||
dummySessions
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(dummySessions.filter { it.isExpired }).hasSize(0)
|
|
||||||
|
|
||||||
userRoleService.deleteUserRole(1)
|
|
||||||
|
|
||||||
verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>())
|
|
||||||
|
|
||||||
assertThat(dummySessions.filter { it.isExpired }).hasSize(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
|
||||||
inner class WithoutExistingUserRole {
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setup() {
|
|
||||||
doAnswer { _ ->
|
|
||||||
Optional.empty<UserRole>()
|
|
||||||
}.whenever(userRoleRepository).findById(any<Long>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotUpdateUserRole() {
|
|
||||||
userRoleService.updateUserRole(1, Role.ADMIN)
|
|
||||||
|
|
||||||
verify(userRoleRepository, never()).save(any<UserRole>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotExpireSessionOnUpdate() {
|
|
||||||
userRoleService.updateUserRole(1, Role.ADMIN)
|
|
||||||
|
|
||||||
verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotDeleteUserRole() {
|
|
||||||
userRoleService.deleteUserRole(1)
|
|
||||||
|
|
||||||
verify(userRoleRepository, never()).delete(any<UserRole>())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotExpireSessionOnDelete() {
|
|
||||||
userRoleService.deleteUserRole(1)
|
|
||||||
|
|
||||||
verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private fun dummyPrincipal() = DefaultOidcUser(
|
|
||||||
listOf(),
|
|
||||||
OidcIdToken(
|
|
||||||
"anytokenvalue",
|
|
||||||
Instant.now(),
|
|
||||||
Instant.now().plusSeconds(10),
|
|
||||||
mapOf("sub" to "testsub", "preferred_username" to "patrick.tester")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun dummySessions() = listOf(
|
|
||||||
SessionInformation(
|
|
||||||
dummyPrincipal(),
|
|
||||||
"SESSIONID1",
|
|
||||||
Date.from(Instant.now()),
|
|
||||||
),
|
|
||||||
SessionInformation(
|
|
||||||
dummyPrincipal(),
|
|
||||||
"SESSIONID2",
|
|
||||||
Date.from(Instant.now()),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
@ -22,14 +22,9 @@ package dev.dnpm.etl.processor.services
|
|||||||
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 dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|
||||||
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
|
||||||
import 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 org.junit.jupiter.params.ParameterizedTest
|
|
||||||
import org.junit.jupiter.params.provider.Arguments
|
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
|
||||||
|
|
||||||
class ReportServiceTest {
|
class ReportServiceTest {
|
||||||
|
|
||||||
@ -65,15 +60,6 @@ class ReportServiceTest {
|
|||||||
assertThat(actual[2].message).isEqualTo("Warning Message")
|
assertThat(actual[2].message).isEqualTo("Warning Message")
|
||||||
assertThat(actual[3].severity).isEqualTo(ReportService.Severity.INFO)
|
assertThat(actual[3].severity).isEqualTo(ReportService.Severity.INFO)
|
||||||
assertThat(actual[3].message).isEqualTo("Info Message")
|
assertThat(actual[3].message).isEqualTo("Info Message")
|
||||||
|
|
||||||
assertThat(actual.asRequestStatus()).isEqualTo(RequestStatus.ERROR)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("testData")
|
|
||||||
fun shouldParseDataQualityReport(json: String, requestStatus: RequestStatus) {
|
|
||||||
val actual = this.reportService.deserialize(json)
|
|
||||||
assertThat(actual.asRequestStatus()).isEqualTo(requestStatus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -87,75 +73,4 @@ class ReportServiceTest {
|
|||||||
assertThat(actual[0].message).isEqualTo("Not parsable data quality report '$invalidResponse'")
|
assertThat(actual[0].message).isEqualTo("Not parsable data quality report '$invalidResponse'")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun testData(): Set<Arguments> {
|
|
||||||
return setOf(
|
|
||||||
Arguments.of(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"patient": "4711",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" },
|
|
||||||
{ "severity": "fatal", "message": "Fatal Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
RequestStatus.ERROR
|
|
||||||
),
|
|
||||||
Arguments.of(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"patient": "4711",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" },
|
|
||||||
{ "severity": "error", "message": "Error Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
RequestStatus.ERROR
|
|
||||||
),
|
|
||||||
Arguments.of(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"patient": "4711",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "error", "message": "Error Message" }
|
|
||||||
{ "severity": "info", "message": "Info Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
RequestStatus.ERROR
|
|
||||||
),
|
|
||||||
Arguments.of(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"patient": "4711",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" },
|
|
||||||
{ "severity": "warning", "message": "Warning Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
RequestStatus.WARNING
|
|
||||||
),
|
|
||||||
Arguments.of(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"patient": "4711",
|
|
||||||
"issues": [
|
|
||||||
{ "severity": "info", "message": "Info Message" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""".trimIndent(),
|
|
||||||
RequestStatus.SUCCESS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -21,34 +21,27 @@ 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.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
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.*
|
import org.mockito.Mockito.*
|
||||||
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.argumentCaptor
|
import org.mockito.kotlin.argumentCaptor
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,28 +88,28 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendMtbFileDuplicationAndSaveUnknownRequestStatusAtFirst() {
|
fun testShouldSendMtbFileDuplicationAndSaveUnknownRequestStatusAtFirst() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga"),
|
fingerprint = "zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
processedAt = Instant.parse("2023-08-08T02:00:00Z")
|
||||||
)
|
)
|
||||||
}.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
|
}.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
false
|
false
|
||||||
}.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
|
}.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(any())
|
||||||
|
|
||||||
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(
|
||||||
@ -157,28 +147,28 @@ class RequestProcessorTest {
|
|||||||
fun testShouldDetectMtbFileDuplicationAndSendDuplicationEvent() {
|
fun testShouldDetectMtbFileDuplicationAndSendDuplicationEvent() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga"),
|
fingerprint = "zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
processedAt = Instant.parse("2023-08-08T02:00:00Z")
|
||||||
)
|
)
|
||||||
}.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
|
}.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
false
|
false
|
||||||
}.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
|
}.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(any())
|
||||||
|
|
||||||
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(
|
||||||
@ -216,32 +206,32 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendMtbFileAndSendSuccessEvent() {
|
fun testShouldSendMtbFileAndSendSuccessEvent() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("different"),
|
fingerprint = "different",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
processedAt = Instant.parse("2023-08-08T02:00:00Z")
|
||||||
)
|
)
|
||||||
}.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
|
}.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
false
|
false
|
||||||
}.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
|
}.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.`when`(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(any())
|
||||||
|
|
||||||
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(
|
||||||
@ -279,32 +269,32 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendMtbFileAndSendErrorEvent() {
|
fun testShouldSendMtbFileAndSendErrorEvent() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("different"),
|
fingerprint = "different",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
processedAt = Instant.parse("2023-08-08T02:00:00Z")
|
||||||
)
|
)
|
||||||
}.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
|
}.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
false
|
false
|
||||||
}.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
|
}.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.`when`(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(any())
|
||||||
|
|
||||||
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(
|
||||||
@ -342,13 +332,13 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() {
|
fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
"PSEUDONYM"
|
"PSEUDONYM"
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.`when`(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion("TEST_12345678901")
|
||||||
|
|
||||||
val requestCaptor = argumentCaptor<Request>()
|
val requestCaptor = argumentCaptor<Request>()
|
||||||
verify(requestService, times(1)).save(requestCaptor.capture())
|
verify(requestService, times(1)).save(requestCaptor.capture())
|
||||||
@ -360,13 +350,13 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendDeleteRequestAndSendSuccessEvent() {
|
fun testShouldSendDeleteRequestAndSendSuccessEvent() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
"PSEUDONYM"
|
"PSEUDONYM"
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.`when`(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion("TEST_12345678901")
|
||||||
|
|
||||||
val eventCaptor = argumentCaptor<ResponseEvent>()
|
val eventCaptor = argumentCaptor<ResponseEvent>()
|
||||||
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
||||||
@ -378,13 +368,13 @@ class RequestProcessorTest {
|
|||||||
fun testShouldSendDeleteRequestAndSendErrorEvent() {
|
fun testShouldSendDeleteRequestAndSendErrorEvent() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
"PSEUDONYM"
|
"PSEUDONYM"
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(anyString())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.`when`(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion("TEST_12345678901")
|
||||||
|
|
||||||
val eventCaptor = argumentCaptor<ResponseEvent>()
|
val eventCaptor = argumentCaptor<ResponseEvent>()
|
||||||
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
|
||||||
@ -394,9 +384,9 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
|
||||||
doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
doThrow(RuntimeException()).`when`(pseudonymizeService).patientPseudonym(anyString())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE)
|
this.requestProcessor.processDeletion("TEST_12345678901")
|
||||||
|
|
||||||
val requestCaptor = argumentCaptor<Request>()
|
val requestCaptor = argumentCaptor<Request>()
|
||||||
verify(requestService, times(1)).save(requestCaptor.capture())
|
verify(requestService, times(1)).save(requestCaptor.capture())
|
||||||
@ -410,15 +400,15 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
}.`when`(pseudonymizeService).patientPseudonym(any())
|
||||||
|
|
||||||
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>())
|
}.`when`(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -452,8 +442,4 @@ class RequestProcessorTest {
|
|||||||
assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
|
assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
}
|
||||||
val TEST_PATIENT_ID = PatientId("TEST_12345678901")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.*
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
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
|
||||||
@ -31,9 +30,8 @@ import org.junit.jupiter.api.extension.ExtendWith
|
|||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.*
|
import org.mockito.Mockito.*
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.anyValueClass
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class RequestServiceTest {
|
class RequestServiceTest {
|
||||||
@ -43,14 +41,14 @@ class RequestServiceTest {
|
|||||||
private lateinit var requestService: RequestService
|
private lateinit var requestService: RequestService
|
||||||
|
|
||||||
private fun anyRequest() = any(Request::class.java) ?: Request(
|
private fun anyRequest() = any(Request::class.java) ?: Request(
|
||||||
0L,
|
id = 0L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_dummy"),
|
patientId = "TEST_dummy",
|
||||||
PatientId("PX"),
|
pid = "PX",
|
||||||
Fingerprint("dummy"),
|
fingerprint = "dummy",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-08-08T02:00:00Z")
|
processedAt = Instant.parse("2023-08-08T02:00:00Z")
|
||||||
)
|
)
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@ -65,34 +63,34 @@ class RequestServiceTest {
|
|||||||
fun shouldIndicateLastRequestIsDeleteRequest() {
|
fun shouldIndicateLastRequestIsDeleteRequest() {
|
||||||
val requests = listOf(
|
val requests = listOf(
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.WARNING,
|
status = RequestStatus.WARNING,
|
||||||
Instant.parse("2023-07-07T00:00:00Z")
|
processedAt = Instant.parse("2023-07-07T00:00:00Z")
|
||||||
),
|
),
|
||||||
Request(
|
Request(
|
||||||
2L,
|
id = 2L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdefd"),
|
fingerprint = "0123456789abcdefd",
|
||||||
RequestType.DELETE,
|
type = RequestType.DELETE,
|
||||||
RequestStatus.WARNING,
|
status = RequestStatus.WARNING,
|
||||||
Instant.parse("2023-07-07T02:00:00Z")
|
processedAt = Instant.parse("2023-07-07T02:00:00Z")
|
||||||
),
|
),
|
||||||
Request(
|
Request(
|
||||||
3L,
|
id = 3L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.UNKNOWN,
|
status = RequestStatus.UNKNOWN,
|
||||||
Instant.parse("2023-08-11T00:00:00Z")
|
processedAt = Instant.parse("2023-08-11T00:00:00Z")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,34 +103,34 @@ class RequestServiceTest {
|
|||||||
fun shouldIndicateLastRequestIsNotDeleteRequest() {
|
fun shouldIndicateLastRequestIsNotDeleteRequest() {
|
||||||
val requests = listOf(
|
val requests = listOf(
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.WARNING,
|
status = RequestStatus.WARNING,
|
||||||
Instant.parse("2023-07-07T00:00:00Z")
|
processedAt = Instant.parse("2023-07-07T00:00:00Z")
|
||||||
),
|
),
|
||||||
Request(
|
Request(
|
||||||
2L,
|
id = 2L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.WARNING,
|
status = RequestStatus.WARNING,
|
||||||
Instant.parse("2023-07-07T02:00:00Z")
|
processedAt = Instant.parse("2023-07-07T02:00:00Z")
|
||||||
),
|
),
|
||||||
Request(
|
Request(
|
||||||
3L,
|
id = 3L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.UNKNOWN,
|
status = RequestStatus.UNKNOWN,
|
||||||
Instant.parse("2023-08-11T00:00:00Z")
|
processedAt = Instant.parse("2023-08-11T00:00:00Z")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -145,31 +143,31 @@ class RequestServiceTest {
|
|||||||
fun shouldReturnPatientsLastRequest() {
|
fun shouldReturnPatientsLastRequest() {
|
||||||
val requests = listOf(
|
val requests = listOf(
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.DELETE,
|
type = RequestType.DELETE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-07-07T02:00:00Z")
|
processedAt = Instant.parse("2023-07-07T02:00:00Z")
|
||||||
),
|
),
|
||||||
Request(
|
Request(
|
||||||
1L,
|
id = 1L,
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678902"),
|
patientId = "TEST_12345678902",
|
||||||
PatientId("P2"),
|
pid = "P2",
|
||||||
Fingerprint("0123456789abcdef2"),
|
fingerprint = "0123456789abcdef2",
|
||||||
RequestType.MTB_FILE,
|
type = RequestType.MTB_FILE,
|
||||||
RequestStatus.WARNING,
|
status = RequestStatus.WARNING,
|
||||||
Instant.parse("2023-08-08T00:00:00Z")
|
processedAt = Instant.parse("2023-08-08T00:00:00Z")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val actual = RequestService.lastMtbFileRequestForPatientPseudonym(requests)
|
val actual = RequestService.lastMtbFileRequestForPatientPseudonym(requests)
|
||||||
|
|
||||||
assertThat(actual).isInstanceOf(Request::class.java)
|
assertThat(actual).isInstanceOf(Request::class.java)
|
||||||
assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef2"))
|
assertThat(actual?.fingerprint).isEqualTo("0123456789abcdef2")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -186,16 +184,16 @@ class RequestServiceTest {
|
|||||||
doAnswer {
|
doAnswer {
|
||||||
val obj = it.arguments[0] as Request
|
val obj = it.arguments[0] as Request
|
||||||
obj.copy(id = 1L)
|
obj.copy(id = 1L)
|
||||||
}.whenever(requestRepository).save(anyRequest())
|
}.`when`(requestRepository).save(anyRequest())
|
||||||
|
|
||||||
val request = Request(
|
val request = Request(
|
||||||
randomRequestId(),
|
uuid = UUID.randomUUID().toString(),
|
||||||
PatientPseudonym("TEST_12345678901"),
|
patientId = "TEST_12345678901",
|
||||||
PatientId("P1"),
|
pid = "P1",
|
||||||
Fingerprint("0123456789abcdef1"),
|
fingerprint = "0123456789abcdef1",
|
||||||
RequestType.DELETE,
|
type = RequestType.DELETE,
|
||||||
RequestStatus.SUCCESS,
|
status = RequestStatus.SUCCESS,
|
||||||
Instant.parse("2023-07-07T02:00:00Z")
|
processedAt = Instant.parse("2023-07-07T02:00:00Z")
|
||||||
)
|
)
|
||||||
|
|
||||||
requestService.save(request)
|
requestService.save(request)
|
||||||
@ -205,23 +203,23 @@ class RequestServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun allRequestsByPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() {
|
fun allRequestsByPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() {
|
||||||
requestService.allRequestsByPatientPseudonym(PatientPseudonym("TEST_12345678901"))
|
requestService.allRequestsByPatientPseudonym("TEST_12345678901")
|
||||||
|
|
||||||
verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass())
|
verify(requestRepository, times(1)).findAllByPatientIdOrderByProcessedAtDesc(anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun lastMtbFileRequestForPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() {
|
fun lastMtbFileRequestForPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() {
|
||||||
requestService.lastMtbFileRequestForPatientPseudonym(PatientPseudonym("TEST_12345678901"))
|
requestService.lastMtbFileRequestForPatientPseudonym("TEST_12345678901")
|
||||||
|
|
||||||
verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass())
|
verify(requestRepository, times(1)).findAllByPatientIdOrderByProcessedAtDesc(anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun isLastRequestDeletionShouldRequestAllRequestsForPatientPseudonym() {
|
fun isLastRequestDeletionShouldRequestAllRequestsForPatientPseudonym() {
|
||||||
requestService.isLastRequestWithKnownStatusDeletion(PatientPseudonym("TEST_12345678901"))
|
requestService.isLastRequestWithKnownStatusDeletion("TEST_12345678901")
|
||||||
|
|
||||||
verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass())
|
verify(requestRepository, times(1)).findAllByPatientIdOrderByProcessedAtDesc(anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.services
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.*
|
|
||||||
import dev.dnpm.etl.processor.monitoring.Request
|
import dev.dnpm.etl.processor.monitoring.Request
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
@ -29,6 +29,7 @@ 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.*
|
||||||
@ -39,64 +40,64 @@ import java.util.*
|
|||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class ResponseProcessorTest {
|
class ResponseProcessorTest {
|
||||||
|
|
||||||
private lateinit var requestService: RequestService
|
private lateinit var requestRepository: RequestRepository
|
||||||
private lateinit var statisticsUpdateProducer: Sinks.Many<Any>
|
private lateinit var statisticsUpdateProducer: Sinks.Many<Any>
|
||||||
|
|
||||||
private lateinit var responseProcessor: ResponseProcessor
|
private lateinit var responseProcessor: ResponseProcessor
|
||||||
|
|
||||||
private val testRequest = Request(
|
private val testRequest = Request(
|
||||||
1L,
|
1L,
|
||||||
RequestId("TestID1234"),
|
"TestID1234",
|
||||||
PatientPseudonym("PSEUDONYM-A"),
|
"PSEUDONYM-A",
|
||||||
PatientId("1"),
|
"1",
|
||||||
Fingerprint("dummyfingerprint"),
|
"dummyfingerprint",
|
||||||
RequestType.MTB_FILE,
|
RequestType.MTB_FILE,
|
||||||
RequestStatus.UNKNOWN
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup(
|
fun setup(
|
||||||
@Mock requestService: RequestService,
|
@Mock requestRepository: RequestRepository,
|
||||||
@Mock statisticsUpdateProducer: Sinks.Many<Any>
|
@Mock statisticsUpdateProducer: Sinks.Many<Any>
|
||||||
) {
|
) {
|
||||||
this.requestService = requestService
|
this.requestRepository = requestRepository
|
||||||
this.statisticsUpdateProducer = statisticsUpdateProducer
|
this.statisticsUpdateProducer = statisticsUpdateProducer
|
||||||
|
|
||||||
this.responseProcessor = ResponseProcessor(requestService, statisticsUpdateProducer)
|
this.responseProcessor = ResponseProcessor(requestRepository, statisticsUpdateProducer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldNotSaveStatusForUnknownRequest() {
|
fun shouldNotSaveStatusForUnknownRequest() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
Optional.empty<Request>()
|
Optional.empty<Request>()
|
||||||
}.whenever(requestService).findByUuid(anyValueClass())
|
}.whenever(requestRepository).findByUuidEquals(anyString())
|
||||||
|
|
||||||
val event = ResponseEvent(
|
val event = ResponseEvent(
|
||||||
RequestId("TestID1234"),
|
"TestID1234",
|
||||||
Instant.parse("2023-09-09T00:00:00Z"),
|
Instant.parse("2023-09-09T00:00:00Z"),
|
||||||
RequestStatus.SUCCESS
|
RequestStatus.SUCCESS
|
||||||
)
|
)
|
||||||
|
|
||||||
this.responseProcessor.handleResponseEvent(event)
|
this.responseProcessor.handleResponseEvent(event)
|
||||||
|
|
||||||
verify(requestService, never()).save(any())
|
verify(requestRepository, never()).save(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldNotSaveStatusWithUnknownState() {
|
fun shouldNotSaveStatusWithUnknownState() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
Optional.of(testRequest)
|
Optional.of(testRequest)
|
||||||
}.whenever(requestService).findByUuid(anyValueClass())
|
}.whenever(requestRepository).findByUuidEquals(anyString())
|
||||||
|
|
||||||
val event = ResponseEvent(
|
val event = ResponseEvent(
|
||||||
RequestId("TestID1234"),
|
"TestID1234",
|
||||||
Instant.parse("2023-09-09T00:00:00Z"),
|
Instant.parse("2023-09-09T00:00:00Z"),
|
||||||
RequestStatus.UNKNOWN
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
|
|
||||||
this.responseProcessor.handleResponseEvent(event)
|
this.responseProcessor.handleResponseEvent(event)
|
||||||
|
|
||||||
verify(requestService, never()).save(any<Request>())
|
verify(requestRepository, never()).save(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ -104,10 +105,10 @@ class ResponseProcessorTest {
|
|||||||
fun shouldSaveStatusForKnownRequest(requestStatus: RequestStatus) {
|
fun shouldSaveStatusForKnownRequest(requestStatus: RequestStatus) {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
Optional.of(testRequest)
|
Optional.of(testRequest)
|
||||||
}.whenever(requestService).findByUuid(anyValueClass())
|
}.whenever(requestRepository).findByUuidEquals(anyString())
|
||||||
|
|
||||||
val event = ResponseEvent(
|
val event = ResponseEvent(
|
||||||
RequestId("TestID1234"),
|
"TestID1234",
|
||||||
Instant.parse("2023-09-09T00:00:00Z"),
|
Instant.parse("2023-09-09T00:00:00Z"),
|
||||||
requestStatus
|
requestStatus
|
||||||
)
|
)
|
||||||
@ -115,7 +116,7 @@ class ResponseProcessorTest {
|
|||||||
this.responseProcessor.handleResponseEvent(event)
|
this.responseProcessor.handleResponseEvent(event)
|
||||||
|
|
||||||
val captor = argumentCaptor<Request>()
|
val captor = argumentCaptor<Request>()
|
||||||
verify(requestService, times(1)).save(captor.capture())
|
verify(requestRepository, times(1)).save(captor.capture())
|
||||||
assertThat(captor.firstValue).isNotNull
|
assertThat(captor.firstValue).isNotNull
|
||||||
assertThat(captor.firstValue.status).isEqualTo(requestStatus)
|
assertThat(captor.firstValue.status).isEqualTo(requestStatus)
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,13 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package dev.dnpm.etl.processor.security
|
package dev.dnpm.etl.processor.services
|
||||||
|
|
||||||
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 org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
import org.mockito.ArgumentMatchers.anyLong
|
import org.mockito.ArgumentMatchers.anyLong
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
@ -95,11 +96,11 @@ class TokenServiceTest {
|
|||||||
|
|
||||||
val actual = this.tokenService.addToken("Test Token")
|
val actual = this.tokenService.addToken("Test Token")
|
||||||
|
|
||||||
val captor = argumentCaptor<Token>()
|
val captor = ArgumentCaptor.forClass(Token::class.java)
|
||||||
verify(tokenRepository, times(1)).save(captor.capture())
|
verify(tokenRepository, times(1)).save(captor.capture())
|
||||||
|
|
||||||
assertThat(actual).satisfies(Consumer { assertThat(it.isSuccess).isTrue() })
|
assertThat(actual).satisfies(Consumer { assertThat(it.isSuccess).isTrue() })
|
||||||
assertThat(captor.firstValue).satisfies(
|
assertThat(captor.value).satisfies(
|
||||||
Consumer { assertThat(it.name).isEqualTo("Test Token") },
|
Consumer { assertThat(it.name).isEqualTo("Test Token") },
|
||||||
Consumer { assertThat(it.username).isEqualTo("testtoken") },
|
Consumer { assertThat(it.username).isEqualTo("testtoken") },
|
||||||
Consumer { assertThat(it.password).isEqualTo("{test}verysecret") }
|
Consumer { assertThat(it.password).isEqualTo("{test}verysecret") }
|
||||||
@ -115,13 +116,13 @@ class TokenServiceTest {
|
|||||||
|
|
||||||
this.tokenService.deleteToken(42)
|
this.tokenService.deleteToken(42)
|
||||||
|
|
||||||
val stringCaptor = argumentCaptor<String>()
|
val stringCaptor = ArgumentCaptor.forClass(String::class.java)
|
||||||
verify(userDetailsManager, times(1)).deleteUser(stringCaptor.capture())
|
verify(userDetailsManager, times(1)).deleteUser(stringCaptor.capture())
|
||||||
assertThat(stringCaptor.firstValue).isEqualTo("testtoken")
|
assertThat(stringCaptor.value).isEqualTo("testtoken")
|
||||||
|
|
||||||
val tokenCaptor = argumentCaptor<Token>()
|
val tokenCaptor = ArgumentCaptor.forClass(Token::class.java)
|
||||||
verify(tokenRepository, times(1)).delete(tokenCaptor.capture())
|
verify(tokenRepository, times(1)).delete(tokenCaptor.capture())
|
||||||
assertThat(tokenCaptor.firstValue.id).isEqualTo(42)
|
assertThat(tokenCaptor.value.id).isEqualTo(42)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
@ -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 one or more lines are too long
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