mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-07-17 21:02:54 +00:00
Compare commits
1 Commits
119-genomd
...
v0.10.0
Author | SHA1 | Date | |
---|---|---|---|
ec096d9c81 |
261
README.md
261
README.md
@ -1,48 +1,30 @@
|
|||||||
# ETL-Processor for DNPM:DIP [](https://github.com/pcvolkmer/etl-processor/actions/workflows/test.yml)
|
# ETL-Processor for DNPM:DIP [](https://github.com/pcvolkmer/etl-processor/actions/workflows/test.yml)
|
||||||
|
|
||||||
Diese Anwendung versendet ein bwHC-MTB-File im bwHC-Datenmodell 1.0 an DNPM:DIP und pseudonymisiert
|
Diese Anwendung versendet ein bwHC-MTB-File im bwHC-Datenmodell 1.0 an DNPM:DIP und pseudonymisiert die Patienten-ID.
|
||||||
die Patienten-ID.
|
|
||||||
|
|
||||||
## Einordnung innerhalb einer DNPM-ETL-Strecke
|
## Einordnung innerhalb einer DNPM-ETL-Strecke
|
||||||
|
|
||||||
Diese Anwendung erlaubt das Entgegennehmen von HTTP/REST-Anfragen aus dem Onkostar-Plugin *
|
Diese Anwendung erlaubt das Entgegennehmen von HTTP/REST-Anfragen aus dem Onkostar-Plugin **[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)**.
|
||||||
*[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)**.
|
|
||||||
|
|
||||||
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 DNPM:DIP weitergeleitet.
|
||||||
|
|
||||||
Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand
|
Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand der Anwendung gewährt.
|
||||||
der Anwendung gewährt.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Duplikaterkennung
|
### Duplikaterkennung
|
||||||
|
|
||||||
Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über den
|
Die Erkennung von Duplikaten ist normalerweise immer aktiv, kann jedoch über den Konfigurationsparameter
|
||||||
Konfigurationsparameter
|
|
||||||
`APP_DUPLICATION_DETECTION=false` deaktiviert werden.
|
`APP_DUPLICATION_DETECTION=false` deaktiviert werden.
|
||||||
|
|
||||||
### Modelvorhaben genomDE §64e
|
|
||||||
|
|
||||||
Zusätzlich zur Patienten Identifier Pseudonymisierung müssen Vorgangsummern generiert werden, die
|
|
||||||
jede Übertragung eindeutig identifizieren aber gleichzeitig dem Patienten zugeordnet werden können.
|
|
||||||
Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Originalwert mehrere
|
|
||||||
Pseudonyme benötigt.
|
|
||||||
Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch
|
|
||||||
*APP_PSEUDONYMIZE_GPAS_CCDN*).
|
|
||||||
|
|
||||||
**WICHTIG:** Deaktivierte Pseudonymisierung ist nur für Tests nutzbar. Vorgangsummern sind zufällig
|
|
||||||
und werden anschließend verworfen.
|
|
||||||
|
|
||||||
### Datenübermittlung über HTTP/REST
|
### Datenübermittlung über HTTP/REST
|
||||||
|
|
||||||
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an DNPM:DIP
|
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung direkt an DNPM:DIP gesendet.
|
||||||
gesendet.
|
|
||||||
|
|
||||||
Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an
|
Ein HTTP Request kann, angenommen die Installation erfolgte auf dem Host `dnpm.example.com` an nachfolgende URLs gesendet werden:
|
||||||
nachfolgende URLs gesendet werden:
|
|
||||||
|
|
||||||
| HTTP-Request | URL | Consent-Status im Datensatz | Bemerkung |
|
| HTTP-Request | URL | Consent-Status im Datensatz | Bemerkung |
|
||||||
|--------------|-----------------------------------------|-----------------------------|---------------------------------------------------------------------------------|
|
|--------------|-----------------------------------------|-----------------------------|---------------------------------------------------------------------------------|
|
||||||
@ -50,15 +32,12 @@ nachfolgende URLs gesendet werden:
|
|||||||
| `POST` | `https://dnpm.example.com/mtb` | `REJECT` | Die Anwendung sendet einen Lösch-Request für die im Datensatz angegebene Pat-ID |
|
| `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` |
|
| `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
|
Anstelle des Pfads `/mtb` kann auch, wie in Version 0.9 und älter üblich, `/mtbfile` verwendet werden.
|
||||||
werden.
|
|
||||||
|
|
||||||
### Datenübermittlung mit Apache Kafka
|
### Datenübermittlung mit Apache Kafka
|
||||||
|
|
||||||
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung an Apache Kafka
|
Anfragen werden, wenn nicht als Duplikat behandelt, nach der Pseudonymisierung an Apache Kafka übergeben.
|
||||||
übergeben.
|
Eine Antwort wird dabei ebenfalls mithilfe von Apache Kafka übermittelt und nach der Entgegennahme verarbeitet.
|
||||||
Eine Antwort wird dabei ebenfalls mithilfe von Apache Kafka übermittelt und nach der Entgegennahme
|
|
||||||
verarbeitet.
|
|
||||||
|
|
||||||
Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc
|
Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc
|
||||||
|
|
||||||
@ -66,19 +45,17 @@ Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc
|
|||||||
|
|
||||||
### 🔥 Wichtige Änderungen in Version 0.10
|
### 🔥 Wichtige Änderungen in Version 0.10
|
||||||
|
|
||||||
Ab Version 0.10 wird [DNPM:DIP](https://github.com/dnpm-dip) unterstützt und als Standardendpunkt
|
Ab Version 0.10 wird [DNPM:DIP](https://github.com/dnpm-dip) unterstützt und als Standardendpunkt verwendet.
|
||||||
verwendet.
|
Soll noch das alte bwHC-Backend verwendet werden, so ist die Umgebungsvariable `APP_REST_IS_BWHC` auf `true` zu setzen.
|
||||||
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
|
### 🔥 Breaking Changes nach Version 0.10
|
||||||
|
|
||||||
In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen
|
In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen entfernt:
|
||||||
entfernt:
|
|
||||||
|
|
||||||
|
* `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`: Nutzen Sie hier, wie unter [_Integration eines eigenen Root CA
|
||||||
|
Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben, das Einbinden eigener Zertifikate.
|
||||||
* `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC`
|
* `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC`
|
||||||
* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption
|
* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`
|
||||||
`APP_KAFKA_OUTPUT_RESPONSE_TOPIC`
|
|
||||||
|
|
||||||
Der Pfad zum Versenden von MTB-Daten ist nun offiziell `/mtb`.
|
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.
|
In Versionen **nach Version 0.10** wird die Unterstützung des Pfads `/mtbfile` entfernt.
|
||||||
@ -91,37 +68,39 @@ 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
|
werden.
|
||||||
aktuellen Kontext nicht
|
* Die Pseudonymisierung erfolgt im ETL-Prozessor nur für die Patienten-ID.
|
||||||
vergleichbare IDs bereitzustellen.
|
Andere IDs werden mithilfe des standortbezogenen Präfixes (erneut) anonymisiert, um für den aktuellen Kontext nicht
|
||||||
|
vergleichbare IDs bereitzustellen.
|
||||||
|
|
||||||
#### Eingebaute Anonymisierung
|
#### Eingebaute Anonymisierung
|
||||||
|
|
||||||
Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die
|
Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die Patienten-ID der
|
||||||
Patienten-ID der
|
entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende "=" - zuzüglich des konfigurierten Präfixes
|
||||||
entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende "=" - zuzüglich des
|
|
||||||
konfigurierten Präfixes
|
|
||||||
als Patienten-Pseudonym verwendet.
|
als Patienten-Pseudonym verwendet.
|
||||||
|
|
||||||
#### Pseudonymisierung mit gPAS
|
#### Pseudonymisierung mit gPAS
|
||||||
|
|
||||||
Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfigurieren.
|
Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfigurieren.
|
||||||
|
|
||||||
* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B.
|
* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B. `http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`)
|
||||||
`http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`)
|
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname
|
||||||
* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname für Patienten ID
|
|
||||||
* `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_GENOM_DE_DOMAIN`: gPas Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (
|
* ~~`APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`~~: **Veraltet** - Root Zertifikat für gPas, falls es dediziert hinzugefügt werden muss.
|
||||||
Clinical data node)
|
**Wird in nach Version 0.10 entfernt**
|
||||||
|
|
||||||
|
Der Konfigurationsparameter `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION` sollte nicht mehr verwendet werden und wird nach
|
||||||
|
Version 0.10 entfernt.
|
||||||
|
Stattdessen sollte das Root Zertifikat wie unter [_Integration eines eigenen Root CA
|
||||||
|
Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben eingebunden werden.
|
||||||
|
|
||||||
### Anmeldung mit einem Passwort
|
### Anmeldung mit einem Passwort
|
||||||
|
|
||||||
Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass
|
Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass bestimmte Bereiche nur nach
|
||||||
bestimmte Bereiche nur nach
|
|
||||||
einem erfolgreichen Login erreichbar sind.
|
einem erfolgreichen Login erreichbar sind.
|
||||||
|
|
||||||
* `APP_SECURITY_ADMIN_USER`: Muss angegeben werden zur Aktivierung der Zugriffsbeschränkung.
|
* `APP_SECURITY_ADMIN_USER`: Muss angegeben werden zur Aktivierung der Zugriffsbeschränkung.
|
||||||
@ -135,34 +114,27 @@ Hier Beispiele für das Beispielpasswort `very-secret`:
|
|||||||
* `{bcrypt}$2y$05$CCkfsMr/wbTleMyjVIK8g.Aa3RCvrvoLXVAsL.f6KeouS88vXD9b6`
|
* `{bcrypt}$2y$05$CCkfsMr/wbTleMyjVIK8g.Aa3RCvrvoLXVAsL.f6KeouS88vXD9b6`
|
||||||
* `{sha256}9a34717f0646b5e9cfcba70055de62edb026ff4f68671ba3db96aa29297d2df5f1a037d58c745657`
|
* `{sha256}9a34717f0646b5e9cfcba70055de62edb026ff4f68671ba3db96aa29297d2df5f1a037d58c745657`
|
||||||
|
|
||||||
Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der
|
Wird kein Administrator-Passwort angegeben, wird ein zufälliger Wert generiert und beim Start der Anwendung in den Logs
|
||||||
Anwendung in den Logs
|
|
||||||
angezeigt.
|
angezeigt.
|
||||||
|
|
||||||
#### Weitere (nicht administrative) Nutzer mit OpenID Connect
|
#### Weitere (nicht administrative) Nutzer mit OpenID Connect
|
||||||
|
|
||||||
Die folgenden Konfigurationsparameter werden benötigt, um die Authentifizierung weiterer Benutzer an
|
Die folgenden Konfigurationsparameter werden benötigt, um die Authentifizierung weiterer Benutzer an einen OIDC-Provider
|
||||||
einen OIDC-Provider
|
|
||||||
zu delegieren.
|
zu delegieren.
|
||||||
Ein Admin-Benutzer muss dabei konfiguriert sein.
|
Ein Admin-Benutzer muss dabei konfiguriert sein.
|
||||||
|
|
||||||
* `APP_SECURITY_ENABLE_OIDC`: Aktiviert die Nutzung von OpenID Connect. Damit sind weitere Parameter
|
* `APP_SECURITY_ENABLE_OIDC`: Aktiviert die Nutzung von OpenID Connect. Damit sind weitere Parameter erforderlich
|
||||||
erforderlich
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_NAME`: Name. Wird beim zusätzlichen Loginbutton angezeigt.
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_NAME`: Name. Wird beim zusätzlichen
|
|
||||||
Loginbutton angezeigt.
|
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_ID`: Client-ID
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_ID`: Client-ID
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SECRET`: Client-Secret
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SECRET`: Client-Secret
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SCOPE[0]`: Hier sollte immer `openid`
|
* `SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_CUSTOM_CLIENT_SCOPE[0]`: Hier sollte immer `openid` angegeben werden.
|
||||||
angegeben werden.
|
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_ISSUER_URI`: Die URI des Providers,
|
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_ISSUER_URI`: Die URI des Providers,
|
||||||
z.B. `https://auth.example.com/realm/example`
|
z.B. `https://auth.example.com/realm/example`
|
||||||
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_USER_NAME_ATTRIBUTE`: Name des Attributes, welches
|
* `SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_CUSTOM_USER_NAME_ATTRIBUTE`: Name des Attributes, welches den Benutzernamen
|
||||||
den Benutzernamen
|
|
||||||
enthält.
|
enthält.
|
||||||
Oft verwendet: `preferred_username`
|
Oft verwendet: `preferred_username`
|
||||||
|
|
||||||
Ist die Nutzung von OpenID Connect konfiguriert, erscheint ein zusätzlicher Login-Button zur Nutzung
|
Ist die Nutzung von OpenID Connect konfiguriert, erscheint ein zusätzlicher Login-Button zur Nutzung mit OpenID Connect
|
||||||
mit OpenID Connect
|
|
||||||
und dem konfigurierten `CLIENT_NAME`.
|
und dem konfigurierten `CLIENT_NAME`.
|
||||||
|
|
||||||

|

|
||||||
@ -173,76 +145,62 @@ zu finden.
|
|||||||
|
|
||||||
#### Rollenbasierte Berechtigungen
|
#### Rollenbasierte Berechtigungen
|
||||||
|
|
||||||
Wird OpenID Connect verwendet, gibt es eine rollenbasierte Berechtigungszuweisung.
|
Wird OpenID Connect verwendet, gibt es eine rollenbasierte Berechtigungszuweisung.
|
||||||
|
|
||||||
Die Standardrolle für neue OIDC-Benutzer kann mit der Option `APP_SECURITY_DEFAULT_USER_ROLE`
|
Die Standardrolle für neue OIDC-Benutzer kann mit der Option `APP_SECURITY_DEFAULT_USER_ROLE` festgelegt werden.
|
||||||
festgelegt werden.
|
|
||||||
Mögliche Werte sind `user` oder `guest`. Standardwert ist `user`.
|
Mögliche Werte sind `user` oder `guest`. Standardwert ist `user`.
|
||||||
|
|
||||||
Benutzer mit der Rolle "Gast" sehen nur die Inhalte, die auch nicht angemeldete Benutzer sehen.
|
Benutzer mit der Rolle "Gast" sehen nur die Inhalte, die auch nicht angemeldete Benutzer sehen.
|
||||||
|
|
||||||
Hierdurch ist es möglich, einzelne Benutzer einzuschränken oder durch Änderung der Standardrolle auf
|
Hierdurch ist es möglich, einzelne Benutzer einzuschränken oder durch Änderung der Standardrolle auf `guest` nur
|
||||||
`guest` nur
|
|
||||||
einzelne Benutzer als vollwertige Nutzer zuzulassen.
|
einzelne Benutzer als vollwertige Nutzer zuzulassen.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Benutzer werden nach dem Entfernen oder der Änderung der vergebenen Rolle automatisch abgemeldet und
|
Benutzer werden nach dem Entfernen oder der Änderung der vergebenen Rolle automatisch abgemeldet und müssen sich neu anmelden.
|
||||||
müssen sich neu anmelden.
|
|
||||||
Sie bekommen dabei wieder die Standardrolle zugewiesen.
|
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
|
Nur Administratoren haben Zugriff auf den Konfigurationsbereich, nur angemeldete Benutzer können die anonymisierte oder
|
||||||
anonymisierte oder
|
|
||||||
pseudonymisierte Patienten-ID sowie den Qualitätsbericht von DNPM:DIP einsehen.
|
pseudonymisierte Patienten-ID sowie den Qualitätsbericht von DNPM:DIP 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.
|
||||||
|
|
||||||
### Tokenbasierte Authentifizierung für MTBFile-Endpunkt
|
### Tokenbasierte Authentifizierung für MTBFile-Endpunkt
|
||||||
|
|
||||||
Die Anwendung unterstützt das Erstellen und Nutzen einer tokenbasierten Authentifizierung für den
|
Die Anwendung unterstützt das Erstellen und Nutzen einer tokenbasierten Authentifizierung für den MTB-File-Endpunkt.
|
||||||
MTB-File-Endpunkt.
|
|
||||||
|
|
||||||
Dies kann mit der Umgebungsvariable `APP_SECURITY_ENABLE_TOKENS` aktiviert (`true` oder `false`)
|
Dies kann mit der Umgebungsvariable `APP_SECURITY_ENABLE_TOKENS` aktiviert (`true` oder `false`) werden
|
||||||
werden
|
|
||||||
und ist als Standardeinstellung nicht aktiv.
|
und ist als Standardeinstellung nicht aktiv.
|
||||||
|
|
||||||
Ist diese Einstellung aktiviert worden, ist es Administratoren möglich, Zugriffstokens für Onkostar
|
Ist diese Einstellung aktiviert worden, ist es Administratoren möglich, Zugriffstokens für Onkostar zu erstellen, die
|
||||||
zu erstellen, die
|
|
||||||
zur Nutzung des MTB-File-Endpunkts eine HTTP-Basic-Authentifizierung voraussetzen.
|
zur Nutzung des MTB-File-Endpunkts eine HTTP-Basic-Authentifizierung voraussetzen.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
In diesem Fall kann der Endpunkt für das Onkostar-Plugin *
|
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:
|
||||||
*[onkostar-plugin-dnpmexport](https://github.com/CCC-MF/onkostar-plugin-dnpmexport)** wie folgt
|
|
||||||
konfiguriert werden:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
|
https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
|
||||||
```
|
```
|
||||||
|
|
||||||
Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information
|
Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information abgelehnt.
|
||||||
abgelehnt.
|
|
||||||
|
|
||||||
Alternativ kann eine Authentifizierung über Benutzername/Passwort oder OIDC erfolgen.
|
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
|
In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht,
|
||||||
wurde und dadurch nicht dem Wert entspricht,
|
|
||||||
der von DNPM:DIP akzeptiert wird.
|
der von DNPM:DIP akzeptiert wird.
|
||||||
|
|
||||||
Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der "
|
Diese Anwendung bietet daher die Möglichkeit, eine Transformation vorzunehmen. Hierzu muss der "Pfad" innerhalb des JSON-MTB-Files angegeben werden und
|
||||||
Pfad" innerhalb des JSON-MTB-Files angegeben werden und
|
|
||||||
welcher Wert wie ersetzt werden soll.
|
welcher Wert wie ersetzt werden soll.
|
||||||
|
|
||||||
Hier ein Beispiel für die erste (Index 0 - weitere dann mit 1,2, ...) Transformationsregel:
|
Hier ein Beispiel für die erste (Index 0 - weitere dann mit 1,2, ...) Transformationsregel:
|
||||||
|
|
||||||
* `APP_TRANSFORMATIONS_0_PATH`: Pfad zum Wert in der JSON-MTB-Datei. Beispiel:
|
* `APP_TRANSFORMATIONS_0_PATH`: Pfad zum Wert in der JSON-MTB-Datei. Beispiel: `diagnoses[*].icd10.version` für **alle** Diagnosen
|
||||||
`diagnoses[*].icd10.version` für **alle** Diagnosen
|
* `APP_TRANSFORMATIONS_0_FROM`: Angabe des Werts, der ersetzt werden soll. Andere Werte bleiben dabei unverändert.
|
||||||
* `APP_TRANSFORMATIONS_0_FROM`: Angabe des Werts, der ersetzt werden soll. Andere Werte bleiben
|
|
||||||
dabei unverändert.
|
|
||||||
* `APP_TRANSFORMATIONS_0_TO`: Angabe des neuen Werts.
|
* `APP_TRANSFORMATIONS_0_TO`: Angabe des neuen Werts.
|
||||||
|
|
||||||
### Mögliche Endpunkte zur Datenübermittlung
|
### Mögliche Endpunkte zur Datenübermittlung
|
||||||
@ -257,61 +215,53 @@ Werden sowohl REST als auch Kafka-Endpunkt konfiguriert, wird nur der REST-Endpu
|
|||||||
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 DNPM:DIP gesendet wird:
|
||||||
|
|
||||||
* `APP_REST_URI`: URI der zu benutzenden API der Backend-Instanz. Zum Beispiel:
|
* `APP_REST_URI`: URI der zu benutzenden API der Backend-Instanz. Zum Beispiel:
|
||||||
* `http://localhost:9000/bwhc/etl/api` für **bwHC Backend**
|
* `http://localhost:9000/bwhc/etl/api` für **bwHC Backend**
|
||||||
* `http://localhost:9000/api` für **dnpm:dip**
|
* `http://localhost:9000/api` für **dnpm:dip**
|
||||||
* `APP_REST_USERNAME`: Basic-Auth-Benutzername für den REST-Endpunkt
|
* `APP_REST_USERNAME`: Basic-Auth-Benutzername für den REST-Endpunkt
|
||||||
* `APP_REST_PASSWORD`: Basic-Auth-Passwort 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**
|
* `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
|
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an ein Kafka-Topic übermittelt wird:
|
||||||
übermittelt wird:
|
|
||||||
|
|
||||||
* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen.
|
* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen.
|
||||||
* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens.
|
Ersetzt ~~`APP_KAFKA_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
|
||||||
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".
|
||||||
* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_
|
Ersetzt ~~`APP_KAFKA_RESPONSE_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
|
||||||
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
|
||||||
|
|
||||||
Wird keine Rückantwort über Apache Kafka empfangen und es gibt keine weitere Möglichkeit den Status
|
Wird keine Rückantwort über Apache Kafka empfangen und es gibt keine weitere Möglichkeit den Status festzustellen, verbleibt der Status auf `UNKNOWN`.
|
||||||
festzustellen, verbleibt der Status auf `UNKNOWN`.
|
|
||||||
|
|
||||||
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`
|
Lässt sich keine Verbindung zu dem Backend aufbauen, wird eine Rückantwort mit Status-Code `900` erwartet, welchen es
|
||||||
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
|
Wird die Umgebungsvariable `APP_KAFKA_INPUT_TOPIC` gesetzt, kann eine Nachricht auch über dieses Kafka-Topic an den ETL-Prozessor übermittelt werden.
|
||||||
Kafka-Topic an den ETL-Prozessor übermittelt werden.
|
|
||||||
|
|
||||||
##### Retention Time
|
##### Retention Time
|
||||||
|
|
||||||
Generell werden in Apache Kafka alle Records entsprechend der Konfiguration vorgehalten.
|
Generell werden in Apache Kafka alle Records entsprechend der Konfiguration vorgehalten.
|
||||||
So wird ohne spezielle Konfiguration ein Record für 7 Tage in Apache Kafka gespeichert.
|
So wird ohne spezielle Konfiguration ein Record für 7 Tage in Apache Kafka gespeichert.
|
||||||
Es sind innerhalb dieses Zeitraums auch alte Informationen weiterhin enthalten, wenn der Consent
|
Es sind innerhalb dieses Zeitraums auch alte Informationen weiterhin enthalten, wenn der Consent später abgelehnt wurde.
|
||||||
später abgelehnt wurde.
|
|
||||||
|
|
||||||
Durch eine entsprechende Konfiguration des Topics kann dies verhindert werden.
|
Durch eine entsprechende Konfiguration des Topics kann dies verhindert werden.
|
||||||
|
|
||||||
Beispiel - auszuführen innerhalb des Kafka-Containers: Löschen alter Records nach einem Tag
|
Beispiel - auszuführen innerhalb des Kafka-Containers: Löschen alter Records nach einem Tag
|
||||||
|
|
||||||
```
|
```
|
||||||
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config retention.ms=86400000
|
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config retention.ms=86400000
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Key based Retention
|
##### Key based Retention
|
||||||
|
|
||||||
Möchten Sie hingegen immer nur die letzte Meldung für einen Patienten und eine Erkrankung in Apache
|
Möchten Sie hingegen immer nur die letzte Meldung für einen Patienten und eine Erkrankung in Apache Kafka vorhalten,
|
||||||
Kafka vorhalten,
|
|
||||||
so ist die nachfolgend genannte Konfiguration der Kafka-Topics hilfreich.
|
so ist die nachfolgend genannte Konfiguration der Kafka-Topics hilfreich.
|
||||||
|
|
||||||
* `retention.ms`: Möglichst kurze Zeit in der alte Records noch erhalten bleiben, z.B. 10 Sekunden
|
|
||||||
10000
|
* `retention.ms`: Möglichst kurze Zeit in der alte Records noch erhalten bleiben, z.B. 10 Sekunden 10000
|
||||||
* `cleanup.policy`: Löschen alter Records und Beibehalten des letzten Records zu einem
|
* `cleanup.policy`: Löschen alter Records und Beibehalten des letzten Records zu einem Key [delete,compact]
|
||||||
Key [delete,compact]
|
|
||||||
|
|
||||||
Beispiele für ein Topic `test`, hier bitte an die verwendeten Topics anpassen.
|
Beispiele für ein Topic `test`, hier bitte an die verwendeten Topics anpassen.
|
||||||
|
|
||||||
@ -320,23 +270,17 @@ kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-co
|
|||||||
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config cleanup.policy=[delete,compact]
|
kafka-configs.sh --bootstrap-server localhost:9092 --alter --topic test --add-config cleanup.policy=[delete,compact]
|
||||||
```
|
```
|
||||||
|
|
||||||
Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger
|
Da als Key eines Records die (pseudonymisierte) Patienten-ID verwendet wird, stehen mit obiger Konfiguration
|
||||||
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
|
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
|
||||||
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.
|
||||||
|
|
||||||
Es steht dann nur noch die jeweils letzten Information zur Verfügung, dass für einen Patienten/eine
|
Es steht dann nur noch die jeweils letzten Information zur Verfügung, dass für einen Patienten/eine Erkrankung
|
||||||
Erkrankung
|
|
||||||
ein Consent-Widerspruch erfolgte.
|
ein Consent-Widerspruch erfolgte.
|
||||||
|
|
||||||
Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen
|
Dieses Vorgehen empfiehlt sich, wenn Sie gespeicherte Records nachgelagert für andere Auswertungen verwenden möchten.
|
||||||
verwenden möchten.
|
|
||||||
|
|
||||||
### Antworten und Statusauswertung
|
### Antworten und Statusauswertung
|
||||||
|
|
||||||
@ -348,12 +292,10 @@ Anfragen an das bwHC-Backend aus Versionen bis 0.9.x wurden wie folgt behandelt:
|
|||||||
| `HTTP 201` | `WARNING` |
|
| `HTTP 201` | `WARNING` |
|
||||||
| `HTTP 400-...` | `ERROR` |
|
| `HTTP 400-...` | `ERROR` |
|
||||||
|
|
||||||
Dies konnte dazu führen, dass zwar mit einem `HTTP 201` geantwortet wurde, aber dennoch in der
|
Dies konnte dazu führen, dass zwar mit einem `HTTP 201` geantwortet wurde, aber dennoch in der Issue-Liste die
|
||||||
Issue-Liste die
|
|
||||||
Severity `error` aufgetaucht ist.
|
Severity `error` aufgetaucht ist.
|
||||||
|
|
||||||
Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste
|
Ab Version 0.10 wird die Issue-Liste der Antwort verwendet und die darion enthaltene höchste Severity-Stufe als Ergebnis verwendet.
|
||||||
Severity-Stufe als Ergebnis verwendet.
|
|
||||||
|
|
||||||
| Höchste Severity | Status |
|
| Höchste Severity | Status |
|
||||||
|------------------|-----------|
|
|------------------|-----------|
|
||||||
@ -363,10 +305,9 @@ Severity-Stufe als Ergebnis verwendet.
|
|||||||
|
|
||||||
## Docker-Images
|
## Docker-Images
|
||||||
|
|
||||||
Diese Anwendung ist auch als Docker-Image
|
Diese Anwendung ist auch als Docker-Image verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor
|
||||||
verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-processor
|
|
||||||
|
|
||||||
### Images lokal bauen
|
### Images lokal bauen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew bootBuildImage
|
./gradlew bootBuildImage
|
||||||
@ -374,25 +315,20 @@ verfügbar: https://github.com/pcvolkmer/etl-processor/pkgs/container/etl-proces
|
|||||||
|
|
||||||
### Integration eines eigenen Root CA Zertifikats
|
### Integration eines eigenen Root CA Zertifikats
|
||||||
|
|
||||||
Wird eine eigene Root CA verwendet, die nicht offiziell signiert ist, wird es zu Problemen beim
|
Wird eine eigene Root CA verwendet, die nicht offiziell signiert ist, wird es zu Problemen beim SSL-Handshake kommen, wenn z.B. gPAS zur Generierung von Pseudonymen verwendet wird.
|
||||||
SSL-Handshake kommen, wenn z.B. gPAS zur Generierung von Pseudonymen verwendet wird.
|
|
||||||
|
|
||||||
Hier bietet es sich an, das Root CA Zertifikat in das Image zu integrieren.
|
Hier bietet es sich an, das Root CA Zertifikat in das Image zu integrieren.
|
||||||
|
|
||||||
#### Integration beim Bauen des Images
|
#### Integration beim Bauen des Images
|
||||||
|
|
||||||
Hier muss die Zeile `"BP_EMBED_CERTS" to "true"` in der Datei `build.gradle.kts` verwendet werden
|
Hier muss die Zeile `"BP_EMBED_CERTS" to "true"` in der Datei `build.gradle.kts` verwendet werden und darf nicht als Kommentar verwendet werden.
|
||||||
und darf nicht als Kommentar verwendet werden.
|
|
||||||
|
|
||||||
Die PEM-Datei mit dem/den Root CA Zertifikat(en) muss dabei im vorbereiteten Verzeichnis [
|
Die PEM-Datei mit dem/den Root CA Zertifikat(en) muss dabei im vorbereiteten Verzeichnis [`bindings/ca-certificates`](bindings/ca-certificates) enthalten sein.
|
||||||
`bindings/ca-certificates`](bindings/ca-certificates) enthalten sein.
|
|
||||||
|
|
||||||
#### Integration zur Laufzeit
|
#### Integration zur Laufzeit
|
||||||
|
|
||||||
Hier muss die Umgebungsvariable `SERVICE_BINDING_ROOT` z.B. auf den Wert `/bindings` gesetzt sein.
|
Hier muss die Umgebungsvariable `SERVICE_BINDING_ROOT` z.B. auf den Wert `/bindings` gesetzt sein.
|
||||||
Zudem muss ein Verzeichnis `bindings/ca-certificates` - analog zum Verzeichnis [
|
Zudem muss ein Verzeichnis `bindings/ca-certificates` - analog zum Verzeichnis [`bindings/ca-certificates`](bindings/ca-certificates) mit einer PEM-Datei als Docker-Volume eingebunden werden.
|
||||||
`bindings/ca-certificates`](bindings/ca-certificates) mit einer PEM-Datei als Docker-Volume
|
|
||||||
eingebunden werden.
|
|
||||||
|
|
||||||
Beispiel für Docker-Compose:
|
Beispiel für Docker-Compose:
|
||||||
|
|
||||||
@ -407,14 +343,12 @@ Beispiel für Docker-Compose:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
*Ausführen als Docker Container:*
|
*Ausführen als Docker Container:*
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ./deploy
|
cd ./deploy
|
||||||
cp env-sample.env .env
|
cp env-sample.env .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Wenn gewünscht, Änderungen in der `.env` vornehmen.
|
Wenn gewünscht, Änderungen in der `.env` vornehmen.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -423,19 +357,15 @@ docker compose up -d
|
|||||||
|
|
||||||
### Einfaches Beispiel für ein eigenes Docker-Compose-File
|
### Einfaches Beispiel für ein eigenes Docker-Compose-File
|
||||||
|
|
||||||
Die Datei [`docs/docker-compose.yml`](docs/docker-compose.yml) zeigt eine einfache Konfiguration für
|
Die Datei [`docs/docker-compose.yml`](docs/docker-compose.yml) zeigt eine einfache Konfiguration für REST-Requests basierend
|
||||||
REST-Requests basierend
|
|
||||||
auf Docker-Compose mit der gestartet werden kann.
|
auf Docker-Compose mit der gestartet werden kann.
|
||||||
|
|
||||||
### Betrieb hinter einem Reverse-Proxy
|
### Betrieb hinter einem Reverse-Proxy
|
||||||
|
|
||||||
Die Anwendung verarbeitet `X-Forwarded`-HTTP-Header und kann daher auch hinter einem Reverse-Proxy
|
Die Anwendung verarbeitet `X-Forwarded`-HTTP-Header und kann daher auch hinter einem Reverse-Proxy betrieben werden.
|
||||||
betrieben werden.
|
|
||||||
|
|
||||||
Dabei werden, je nachdem welche Header durch den Reverse-Proxy gesendet werden auch Protokoll, Host
|
Dabei werden, je nachdem welche Header durch den Reverse-Proxy gesendet werden auch Protokoll, Host oder auch Path-Präfix
|
||||||
oder auch Path-Präfix
|
automatisch erkannt und verwendet werden. Dadurch ist z.B. eine abweichende Angabe des Pfads problemlos möglich.
|
||||||
automatisch erkannt und verwendet werden. Dadurch ist z.B. eine abweichende Angabe des Pfads
|
|
||||||
problemlos möglich.
|
|
||||||
|
|
||||||
#### Beispiel *Traefik* (mit Docker-Labels):
|
#### Beispiel *Traefik* (mit Docker-Labels):
|
||||||
|
|
||||||
@ -471,17 +401,13 @@ Das folgende Beispiel zeigt die Konfiguration einer _location_ in einer nginx-Ko
|
|||||||
|
|
||||||
## Entwicklungssetup
|
## Entwicklungssetup
|
||||||
|
|
||||||
Zum Starten einer lokalen Entwicklungs- und Testumgebung kann die beiliegende Datei
|
Zum Starten einer lokalen Entwicklungs- und Testumgebung kann die beiliegende Datei `dev-compose.yml` verwendet werden.
|
||||||
`dev-compose.yml` verwendet werden.
|
|
||||||
Diese kann zur Nutzung der Datenbanken **MariaDB** als auch **PostgreSQL** angepasst werden.
|
Diese kann zur Nutzung der Datenbanken **MariaDB** als auch **PostgreSQL** angepasst werden.
|
||||||
|
|
||||||
Zur Nutzung von Apache Kafka muss dazu ein Eintrag im hosts-File vorgenommen werden und der Hostname
|
Zur Nutzung von Apache Kafka muss dazu ein Eintrag im hosts-File vorgenommen werden und der Hostname `kafka` auf die lokale
|
||||||
`kafka` auf die lokale
|
IP-Adresse verweisen. Ohne diese Einstellung ist eine Nutzung von Apache Kafka außerhalb der Docker-Umgebung nicht möglich.
|
||||||
IP-Adresse verweisen. Ohne diese Einstellung ist eine Nutzung von Apache Kafka außerhalb der
|
|
||||||
Docker-Umgebung nicht möglich.
|
|
||||||
|
|
||||||
Beim Start der Anwendung mit dem Profil `dev` wird die in `dev-compose.yml` definierte Umgebung beim
|
Beim Start der Anwendung mit dem Profil `dev` wird die in `dev-compose.yml` definierte Umgebung beim Start der
|
||||||
Start der
|
|
||||||
Anwendung mit gestartet:
|
Anwendung mit gestartet:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -493,5 +419,4 @@ 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
|
Ein einfaches Entwickler-Setup inklusive DNPM:DIP ist mit Hilfe von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar.
|
||||||
von https://github.com/pcvolkmer/dnpmdip-devenv realisierbar.
|
|
@ -5,7 +5,7 @@ import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
war
|
war
|
||||||
id("org.springframework.boot") version "3.5.3"
|
id("org.springframework.boot") version "3.3.10"
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
kotlin("jvm") version "1.9.25"
|
kotlin("jvm") version "1.9.25"
|
||||||
kotlin("plugin.spring") version "1.9.25"
|
kotlin("plugin.spring") version "1.9.25"
|
||||||
@ -13,12 +13,12 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.dnpm"
|
group = "dev.dnpm"
|
||||||
version = "0.11.0-SNAPSHOT"
|
version = "0.10.0"
|
||||||
|
|
||||||
var versions = mapOf(
|
var versions = mapOf(
|
||||||
"bwhc-dto-java" to "0.4.0",
|
"bwhc-dto-java" to "0.4.0",
|
||||||
"mtb-dto" to "0.1.0-SNAPSHOT",
|
|
||||||
"hapi-fhir" to "7.6.0",
|
"hapi-fhir" to "7.6.0",
|
||||||
|
"commons-compress" to "1.26.2",
|
||||||
"mockito-kotlin" to "5.4.0",
|
"mockito-kotlin" to "5.4.0",
|
||||||
"archunit" to "1.3.0",
|
"archunit" to "1.3.0",
|
||||||
// Webjars
|
// Webjars
|
||||||
@ -49,18 +49,9 @@ configurations {
|
|||||||
compileOnly {
|
compileOnly {
|
||||||
extendsFrom(configurations.annotationProcessor.get())
|
extendsFrom(configurations.annotationProcessor.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
all {
|
|
||||||
resolutionStrategy {
|
|
||||||
cacheChangingModulesFor(5, "minutes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
|
||||||
url = uri("https://git.dnpm.dev/api/packages/public-snapshots/maven")
|
|
||||||
}
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://git.dnpm.dev/api/packages/public/maven")
|
url = uri("https://git.dnpm.dev/api/packages/public/maven")
|
||||||
}
|
}
|
||||||
@ -82,7 +73,6 @@ dependencies {
|
|||||||
implementation("commons-codec:commons-codec")
|
implementation("commons-codec:commons-codec")
|
||||||
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||||
implementation("de.ukw.ccc:bwhc-dto-java:${versions["bwhc-dto-java"]}")
|
implementation("de.ukw.ccc:bwhc-dto-java:${versions["bwhc-dto-java"]}")
|
||||||
implementation("dev.pcvolkmer.mv64e:mtb-dto:${versions["mtb-dto"]}") { isChanging = true }
|
|
||||||
implementation("ca.uhn.hapi.fhir:hapi-fhir-base:${versions["hapi-fhir"]}")
|
implementation("ca.uhn.hapi.fhir:hapi-fhir-base:${versions["hapi-fhir"]}")
|
||||||
implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:${versions["hapi-fhir"]}")
|
implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:${versions["hapi-fhir"]}")
|
||||||
implementation("org.apache.httpcomponents.client5:httpclient5")
|
implementation("org.apache.httpcomponents.client5:httpclient5")
|
||||||
@ -90,7 +80,6 @@ dependencies {
|
|||||||
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
|
implementation("org.webjars:webjars-locator:${versions["webjars-locator"]}")
|
||||||
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"]}")
|
||||||
implementation ("org.apache.commons:commons-math3:3.6.1")
|
|
||||||
|
|
||||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
@ -110,8 +99,10 @@ dependencies {
|
|||||||
integrationTestImplementation("org.testcontainers:junit-jupiter")
|
integrationTestImplementation("org.testcontainers:junit-jupiter")
|
||||||
integrationTestImplementation("org.testcontainers:postgresql")
|
integrationTestImplementation("org.testcontainers:postgresql")
|
||||||
integrationTestImplementation("com.tngtech.archunit:archunit:${versions["archunit"]}")
|
integrationTestImplementation("com.tngtech.archunit:archunit:${versions["archunit"]}")
|
||||||
integrationTestImplementation("org.htmlunit:htmlunit")
|
integrationTestImplementation("net.sourceforge.htmlunit:htmlunit")
|
||||||
integrationTestImplementation("org.springframework:spring-webflux")
|
integrationTestImplementation("org.springframework:spring-webflux")
|
||||||
|
// Override dependency version from org.testcontainers:junit-jupiter - CVE-2024-26308, CVE-2024-25710
|
||||||
|
integrationTestImplementation("org.apache.commons:commons-compress:${versions["commons-compress"]}")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
@ -128,9 +119,8 @@ tasks.withType<Test> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Test>("integrationTest") {
|
task<Test>("integrationTest") {
|
||||||
description = "Runs integration tests"
|
description = "Runs integration tests"
|
||||||
group = "verification"
|
|
||||||
|
|
||||||
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
|
||||||
classpath = sourceSets["integrationTest"].runtimeClasspath
|
classpath = sourceSets["integrationTest"].runtimeClasspath
|
||||||
|
@ -23,7 +23,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.dnpm.etl.processor.output.BwhcV1MtbFileRequest
|
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
@ -34,10 +33,10 @@ import org.mockito.kotlin.*
|
|||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.post
|
import org.springframework.test.web.servlet.post
|
||||||
@ -46,7 +45,7 @@ import org.testcontainers.junit.jupiter.Testcontainers
|
|||||||
@Testcontainers
|
@Testcontainers
|
||||||
@ExtendWith(SpringExtension::class)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@MockitoBean(types = [MtbFileSender::class])
|
@MockBean(MtbFileSender::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.rest.uri=http://example.com",
|
"app.rest.uri=http://example.com",
|
||||||
@ -74,7 +73,7 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
)
|
)
|
||||||
inner class TransformationTest {
|
inner class TransformationTest {
|
||||||
|
|
||||||
@MockitoBean
|
@MockBean
|
||||||
private lateinit var mtbFileSender: MtbFileSender
|
private lateinit var mtbFileSender: MtbFileSender
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -92,7 +91,7 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
fun mtbFileIsTransformed() {
|
fun mtbFileIsTransformed() {
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(RequestStatus.SUCCESS)
|
MtbFileSender.Response(RequestStatus.SUCCESS)
|
||||||
}.whenever(mtbFileSender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(mtbFileSender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -135,9 +134,9 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val captor = argumentCaptor<BwhcV1MtbFileRequest>()
|
val captor = argumentCaptor<MtbFileSender.MtbFileRequest>()
|
||||||
verify(mtbFileSender).send(captor.capture())
|
verify(mtbFileSender).send(captor.capture())
|
||||||
assertThat(captor.firstValue.content.diagnoses).hasSize(1).allMatch { diagnosis ->
|
assertThat(captor.firstValue.mtbFile.diagnoses).hasSize(1).allMatch { diagnosis ->
|
||||||
diagnosis.icd10.version == "2014"
|
diagnosis.icd10.version == "2014"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,9 @@ import dev.dnpm.etl.processor.output.KafkaMtbFileSender
|
|||||||
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
import dev.dnpm.etl.processor.output.RestMtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
|
||||||
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
|
||||||
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.dnpm.etl.processor.security.TokenRepository
|
import dev.dnpm.etl.processor.security.TokenRepository
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -36,25 +36,24 @@ import org.junit.jupiter.api.assertThrows
|
|||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException
|
||||||
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
|
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBeans
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@ContextConfiguration(
|
@ContextConfiguration(classes = [
|
||||||
classes = [
|
AppConfiguration::class,
|
||||||
AppConfiguration::class,
|
AppSecurityConfiguration::class,
|
||||||
AppSecurityConfiguration::class,
|
KafkaAutoConfiguration::class,
|
||||||
KafkaAutoConfiguration::class,
|
AppKafkaConfiguration::class,
|
||||||
AppKafkaConfiguration::class,
|
AppRestConfiguration::class
|
||||||
AppRestConfiguration::class
|
])
|
||||||
]
|
@MockBean(ObjectMapper::class)
|
||||||
)
|
|
||||||
@MockitoBean(types = [ObjectMapper::class])
|
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
@ -87,7 +86,7 @@ class AppConfigurationTest {
|
|||||||
"app.kafka.group-id=test"
|
"app.kafka.group-id=test"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(types = [RequestRepository::class])
|
@MockBean(RequestRepository::class)
|
||||||
inner class AppConfigurationKafkaTest(private val context: ApplicationContext) {
|
inner class AppConfigurationKafkaTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -146,7 +145,7 @@ class AppConfigurationTest {
|
|||||||
"app.kafka.group-id=test"
|
"app.kafka.group-id=test"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(types = [RequestProcessor::class])
|
@MockBean(RequestProcessor::class)
|
||||||
inner class AppConfigurationUsingKafkaInputTest(private val context: ApplicationContext) {
|
inner class AppConfigurationUsingKafkaInputTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -182,7 +181,40 @@ class AppConfigurationTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin"
|
"app.pseudonymize.generator=",
|
||||||
|
"app.pseudonymizer=buildin",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
inner class AppConfigurationPseudonymizerBuildinTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldUseConfiguredGenerator() {
|
||||||
|
assertThat(context.getBean(AnonymizingGenerator::class.java)).isNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestPropertySource(
|
||||||
|
properties = [
|
||||||
|
"app.pseudonymize.generator=",
|
||||||
|
"app.pseudonymizer=gpas",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
inner class AppConfigurationPseudonymizerGpasTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldUseConfiguredGenerator() {
|
||||||
|
assertThat(context.getBean(GpasPseudonymGenerator::class.java)).isNotNull
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestPropertySource(
|
||||||
|
properties = [
|
||||||
|
"app.pseudonymize.generator=buildin",
|
||||||
|
"app.pseudonymizer=",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class AppConfigurationPseudonymizeGeneratorBuildinTest(private val context: ApplicationContext) {
|
inner class AppConfigurationPseudonymizeGeneratorBuildinTest(private val context: ApplicationContext) {
|
||||||
@ -197,7 +229,8 @@ class AppConfigurationTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=gpas"
|
"app.pseudonymize.generator=gpas",
|
||||||
|
"app.pseudonymizer=",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) {
|
inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) {
|
||||||
@ -215,13 +248,11 @@ class AppConfigurationTest {
|
|||||||
"app.security.enable-tokens=true"
|
"app.security.enable-tokens=true"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBeans(value = [
|
||||||
types = [
|
MockBean(InMemoryUserDetailsManager::class),
|
||||||
InMemoryUserDetailsManager::class,
|
MockBean(PasswordEncoder::class),
|
||||||
PasswordEncoder::class,
|
MockBean(TokenRepository::class)
|
||||||
TokenRepository::class
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class AppConfigurationTokenEnabledTest(private val context: ApplicationContext) {
|
inner class AppConfigurationTokenEnabledTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -232,13 +263,11 @@ class AppConfigurationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@MockitoBean(
|
@MockBeans(value = [
|
||||||
types = [
|
MockBean(InMemoryUserDetailsManager::class),
|
||||||
InMemoryUserDetailsManager::class,
|
MockBean(PasswordEncoder::class),
|
||||||
PasswordEncoder::class,
|
MockBean(TokenRepository::class)
|
||||||
TokenRepository::class
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
inner class AppConfigurationTokenDisabledTest(private val context: ApplicationContext) {
|
inner class AppConfigurationTokenDisabledTest(private val context: ApplicationContext) {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -37,13 +37,13 @@ import org.mockito.kotlin.times
|
|||||||
import org.mockito.kotlin.verify
|
import org.mockito.kotlin.verify
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.delete
|
import org.springframework.test.web.servlet.delete
|
||||||
@ -57,7 +57,7 @@ import org.springframework.test.web.servlet.post
|
|||||||
AppSecurityConfiguration::class
|
AppSecurityConfiguration::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(types = [TokenRepository::class, RequestProcessor::class])
|
@MockBean(TokenRepository::class, RequestProcessor::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
@ -91,7 +91,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -104,7 +104,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -117,7 +117,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isUnauthorized() }
|
status { isUnauthorized() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, never()).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -130,7 +130,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isForbidden() }
|
status { isForbidden() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, never()).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, never()).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -156,7 +156,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::class])
|
@MockBean(UserRoleRepository::class, ClientRegistrationRepository::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=BUILDIN",
|
"app.pseudonymize.generator=BUILDIN",
|
||||||
@ -177,7 +177,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -190,7 +190,7 @@ class MtbFileRestControllerTest {
|
|||||||
status { isAccepted() }
|
status { isAccepted() }
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ import org.junit.jupiter.api.extension.ExtendWith
|
|||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
|
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
|
||||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
@ -39,7 +39,7 @@ import java.time.Instant
|
|||||||
@DataJdbcTest
|
@DataJdbcTest
|
||||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||||
@Transactional
|
@Transactional
|
||||||
@MockitoBean(types = [MtbFileSender::class])
|
@MockBean(MtbFileSender::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin",
|
"app.pseudonymize.generator=buildin",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -47,8 +47,9 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun setup() {
|
fun setup() {
|
||||||
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
|
||||||
val gPasConfigProperties = GPasConfigProperties(
|
val gPasConfigProperties = GPasConfigProperties(
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
"http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
|
||||||
"test", "test2",
|
"test",
|
||||||
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
@ -62,15 +63,9 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun shouldReturnExpectedPseudonym() {
|
fun shouldReturnExpectedPseudonym() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.OK).body(
|
withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890"))
|
||||||
getDummyResponseBody(
|
|
||||||
"1234",
|
|
||||||
"test",
|
|
||||||
"test1234ABCDEF567890"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +76,7 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun shouldThrowExceptionIfGpasNotAvailable() {
|
fun shouldThrowExceptionIfGpasNotAvailable() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withException(IOException("Simulated IO error")).createResponse(it)
|
withException(IOException("Simulated IO error")).createResponse(it)
|
||||||
}
|
}
|
||||||
@ -93,13 +88,10 @@ class GpasPseudonymGeneratorTest {
|
|||||||
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
|
fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
|
||||||
this.mockRestServiceServer.expect {
|
this.mockRestServiceServer.expect {
|
||||||
method(HttpMethod.POST)
|
method(HttpMethod.POST)
|
||||||
requestTo("https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
}.andRespond {
|
}.andRespond {
|
||||||
withStatus(HttpStatus.FOUND)
|
withStatus(HttpStatus.FOUND)
|
||||||
.header(
|
.header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
|
||||||
HttpHeaders.LOCATION,
|
|
||||||
"https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate"
|
|
||||||
)
|
|
||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import org.testcontainers.junit.jupiter.Testcontainers
|
import org.testcontainers.junit.jupiter.Testcontainers
|
||||||
@ -42,7 +42,7 @@ import java.time.Instant
|
|||||||
@ExtendWith(SpringExtension::class)
|
@ExtendWith(SpringExtension::class)
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@Transactional
|
@Transactional
|
||||||
@MockitoBean(types = [MtbFileSender::class])
|
@MockBean(MtbFileSender::class)
|
||||||
@TestPropertySource(
|
@TestPropertySource(
|
||||||
properties = [
|
properties = [
|
||||||
"app.pseudonymize.generator=buildin",
|
"app.pseudonymize.generator=buildin",
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
|
||||||
@ -27,13 +29,11 @@ import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
|
|||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
import dev.dnpm.etl.processor.security.Role
|
import dev.dnpm.etl.processor.security.Role
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
|
||||||
import dev.dnpm.etl.processor.security.UserRoleService
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
|
import dev.dnpm.etl.processor.security.UserRoleService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.htmlunit.html.HtmlPage
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -46,6 +46,7 @@ import org.mockito.kotlin.verify
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
||||||
@ -54,7 +55,6 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ
|
|||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.test.web.servlet.*
|
import org.springframework.test.web.servlet.*
|
||||||
@ -81,16 +81,14 @@ abstract class MockSink : Sinks.Many<Boolean>
|
|||||||
"app.pseudonymize.generator=BUILDIN"
|
"app.pseudonymize.generator=BUILDIN"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(name = "configsUpdateProducer", types = [MockSink::class])
|
@MockBean(name = "configsUpdateProducer", classes = [MockSink::class])
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [
|
Generator::class,
|
||||||
Generator::class,
|
MtbFileSender::class,
|
||||||
MtbFileSender::class,
|
RequestProcessor::class,
|
||||||
RequestProcessor::class,
|
TransformationService::class,
|
||||||
TransformationService::class,
|
GPasConnectionCheckService::class,
|
||||||
GPasConnectionCheckService::class,
|
RestConnectionCheckService::class,
|
||||||
RestConnectionCheckService::class
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
class ConfigControllerTest {
|
class ConfigControllerTest {
|
||||||
|
|
||||||
@ -145,10 +143,8 @@ class ConfigControllerTest {
|
|||||||
"app.security.admin-user=admin"
|
"app.security.admin-user=admin"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [
|
TokenService::class
|
||||||
TokenService::class
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
inner class WithTokensEnabled {
|
inner class WithTokensEnabled {
|
||||||
private lateinit var tokenService: TokenService
|
private lateinit var tokenService: TokenService
|
||||||
@ -256,10 +252,8 @@ class ConfigControllerTest {
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [
|
UserRoleService::class
|
||||||
UserRoleService::class
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
inner class WithUserRolesEnabled {
|
inner class WithUserRolesEnabled {
|
||||||
private lateinit var userRoleService: UserRoleService
|
private lateinit var userRoleService: UserRoleService
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||||
import dev.dnpm.etl.processor.*
|
import dev.dnpm.etl.processor.*
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
@ -28,8 +30,6 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|||||||
import dev.dnpm.etl.processor.monitoring.RequestType
|
import dev.dnpm.etl.processor.monitoring.RequestType
|
||||||
import dev.dnpm.etl.processor.services.RequestService
|
import dev.dnpm.etl.processor.services.RequestService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.htmlunit.html.HtmlPage
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -40,13 +40,13 @@ import org.mockito.kotlin.any
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.PageImpl
|
import org.springframework.data.domain.PageImpl
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.security.test.context.support.WithMockUser
|
import org.springframework.security.test.context.support.WithMockUser
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
@ -71,8 +71,8 @@ import java.util.*
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [RequestService::class]
|
RequestService::class
|
||||||
)
|
)
|
||||||
class HomeControllerTest {
|
class HomeControllerTest {
|
||||||
|
|
||||||
|
@ -19,21 +19,21 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
|
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.htmlunit.html.HtmlPage
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
@ -56,8 +56,8 @@ import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
|
|||||||
"app.security.enable-tokens=true"
|
"app.security.enable-tokens=true"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [TokenService::class]
|
TokenService::class,
|
||||||
)
|
)
|
||||||
class LoginControllerTest {
|
class LoginControllerTest {
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.web
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import com.gargoylesoftware.htmlunit.WebClient
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import org.htmlunit.WebClient
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
@ -41,10 +41,10 @@ import org.mockito.kotlin.doAnswer
|
|||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
import org.springframework.http.MediaType.TEXT_EVENT_STREAM
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
import org.springframework.test.context.TestPropertySource
|
import org.springframework.test.context.TestPropertySource
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean
|
|
||||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
@ -74,8 +74,8 @@ import java.time.temporal.ChronoUnit
|
|||||||
"app.security.admin-password={noop}very-secret"
|
"app.security.admin-password={noop}very-secret"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@MockitoBean(
|
@MockBean(
|
||||||
types = [RequestService::class]
|
RequestService::class
|
||||||
)
|
)
|
||||||
class StatisticsRestControllerTest {
|
class StatisticsRestControllerTest {
|
||||||
|
|
||||||
|
@ -23,6 +23,4 @@ public interface Generator {
|
|||||||
|
|
||||||
String generate(String id);
|
String generate(String id);
|
||||||
|
|
||||||
String generateGenomDeTan(String id);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -38,41 +38,30 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
|
|
||||||
private final static FhirContext r4Context = FhirContext.forR4();
|
private final static FhirContext r4Context = FhirContext.forR4();
|
||||||
private final String gPasUrl;
|
private final String gPasUrl;
|
||||||
|
private final String psnTargetDomain;
|
||||||
private final HttpHeaders httpHeader;
|
private final HttpHeaders httpHeader;
|
||||||
private final RetryTemplate retryTemplate;
|
private final RetryTemplate retryTemplate;
|
||||||
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
|
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
|
||||||
private final RestTemplate restTemplate;
|
|
||||||
private final @NotNull String genomDeDomain;
|
|
||||||
private final @NotNull String psnTargetDomain;
|
|
||||||
|
|
||||||
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
|
private final RestTemplate restTemplate;
|
||||||
RestTemplate restTemplate) {
|
|
||||||
|
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) {
|
||||||
this.retryTemplate = retryTemplate;
|
this.retryTemplate = retryTemplate;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
this.gPasUrl = gpasCfg.getUri();
|
this.gPasUrl = gpasCfg.getUri();
|
||||||
this.psnTargetDomain = gpasCfg.getTarget();
|
this.psnTargetDomain = gpasCfg.getTarget();
|
||||||
this.genomDeDomain = gpasCfg.getGenomDeDomain();
|
|
||||||
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
|
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
|
||||||
|
|
||||||
log.debug("{} has been initialized", this.getClass().getName());
|
log.debug(String.format("%s has been initialized", this.getClass().getName()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String generate(String id) {
|
public String generate(String id) {
|
||||||
return generate(id, psnTargetDomain);
|
var gPasRequestBody = getGpasRequestBody(id);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String generateGenomDeTan(String id) {
|
|
||||||
return generate(id, genomDeDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String generate(String id, String targetDomain) {
|
|
||||||
var gPasRequestBody = getGpasRequestBody(id, targetDomain);
|
|
||||||
var responseEntity = getGpasPseudonym(gPasRequestBody);
|
var responseEntity = getGpasPseudonym(gPasRequestBody);
|
||||||
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
|
||||||
.parseResource(responseEntity.getBody());
|
.parseResource(responseEntity.getBody());
|
||||||
|
|
||||||
return unwrapPseudonym(gPasPseudonymResult);
|
return unwrapPseudonym(gPasPseudonymResult);
|
||||||
}
|
}
|
||||||
@ -86,9 +75,9 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final var identifier = (Identifier) parameters.get().getPart().stream()
|
final var identifier = (Identifier) parameters.get().getPart().stream()
|
||||||
.filter(a -> a.getName().equals("pseudonym"))
|
.filter(a -> a.getName().equals("pseudonym"))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseGet(ParametersParameterComponent::new).getValue();
|
.orElseGet(ParametersParameterComponent::new).getValue();
|
||||||
|
|
||||||
// pseudonym
|
// pseudonym
|
||||||
return sanitizeValue(identifier.getValue());
|
return sanitizeValue(identifier.getValue());
|
||||||
@ -117,8 +106,8 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
responseEntity = retryTemplate.execute(
|
responseEntity = retryTemplate.execute(
|
||||||
ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
|
ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
|
||||||
String.class));
|
String.class));
|
||||||
|
|
||||||
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
if (responseEntity.getStatusCode().is2xxSuccessful()) {
|
||||||
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
|
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
|
||||||
@ -130,16 +119,16 @@ public class GpasPseudonymGenerator implements Generator {
|
|||||||
return responseEntity;
|
return responseEntity;
|
||||||
} catch (Exception unexpected) {
|
} catch (Exception unexpected) {
|
||||||
throw new PseudonymRequestFailed(
|
throw new PseudonymRequestFailed(
|
||||||
"API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
|
"API request due unexpected error unsuccessful gPas unsuccessful.", unexpected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getGpasRequestBody(String id, String targetDomain) {
|
protected String getGpasRequestBody(String id) {
|
||||||
var requestParameters = new Parameters();
|
var requestParameters = new Parameters();
|
||||||
requestParameters.addParameter().setName("target")
|
requestParameters.addParameter().setName("target")
|
||||||
.setValue(new StringType().setValue(targetDomain));
|
.setValue(new StringType().setValue(psnTargetDomain));
|
||||||
requestParameters.addParameter().setName("original")
|
requestParameters.addParameter().setName("original")
|
||||||
.setValue(new StringType().setValue(id));
|
.setValue(new StringType().setValue(id));
|
||||||
final IParser iParser = r4Context.newJsonParser();
|
final IParser iParser = r4Context.newJsonParser();
|
||||||
return iParser.encodeResourceToString(requestParameters);
|
return iParser.encodeResourceToString(requestParameters);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -48,9 +54,12 @@ data class PseudonymizeConfigProperties(
|
|||||||
data class GPasConfigProperties(
|
data class GPasConfigProperties(
|
||||||
val uri: String?,
|
val uri: String?,
|
||||||
val target: String = "etl-processor",
|
val target: String = "etl-processor",
|
||||||
val genomDeDomain: String = "ccdn",
|
|
||||||
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"
|
||||||
@ -73,8 +82,18 @@ data class RestTargetProperties(
|
|||||||
data class KafkaProperties(
|
data class KafkaProperties(
|
||||||
val inputTopic: String?,
|
val inputTopic: String?,
|
||||||
val outputTopic: String = "etl-processor",
|
val outputTopic: String = "etl-processor",
|
||||||
|
@get:DeprecatedConfigurationProperty(
|
||||||
|
reason = "Deprecated",
|
||||||
|
replacement = "outputTopic"
|
||||||
|
)
|
||||||
|
val topic: String = outputTopic,
|
||||||
val outputResponseTopic: String = "${outputTopic}_response",
|
val outputResponseTopic: String = "${outputTopic}_response",
|
||||||
val groupId: String = "${outputTopic}_group",
|
@get:DeprecatedConfigurationProperty(
|
||||||
|
reason = "Deprecated",
|
||||||
|
replacement = "outputResponseTopic"
|
||||||
|
)
|
||||||
|
val responseTopic: String = outputResponseTopic,
|
||||||
|
val groupId: String = "${topic}_group",
|
||||||
val servers: String = ""
|
val servers: String = ""
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -32,6 +32,12 @@ import dev.dnpm.etl.processor.security.TokenRepository
|
|||||||
import dev.dnpm.etl.processor.security.TokenService
|
import dev.dnpm.etl.processor.security.TokenService
|
||||||
import dev.dnpm.etl.processor.services.Transformation
|
import dev.dnpm.etl.processor.services.Transformation
|
||||||
import dev.dnpm.etl.processor.services.TransformationService
|
import dev.dnpm.etl.processor.services.TransformationService
|
||||||
|
import org.apache.hc.client5.http.impl.classic.HttpClients
|
||||||
|
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager
|
||||||
|
import org.apache.hc.client5.http.socket.ConnectionSocketFactory
|
||||||
|
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory
|
||||||
|
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory
|
||||||
|
import org.apache.hc.core5.http.config.RegistryBuilder
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
@ -39,6 +45,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
|
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
|
||||||
import org.springframework.retry.RetryCallback
|
import org.springframework.retry.RetryCallback
|
||||||
import org.springframework.retry.RetryContext
|
import org.springframework.retry.RetryContext
|
||||||
import org.springframework.retry.RetryListener
|
import org.springframework.retry.RetryListener
|
||||||
@ -51,6 +58,13 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
|||||||
import org.springframework.web.client.HttpClientErrorException
|
import org.springframework.web.client.HttpClientErrorException
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
@ -76,6 +90,18 @@ class AppConfiguration {
|
|||||||
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
|
||||||
@Bean
|
@Bean
|
||||||
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
||||||
|
try {
|
||||||
|
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
||||||
|
return GpasPseudonymGenerator(
|
||||||
|
configProperties,
|
||||||
|
retryTemplate,
|
||||||
|
createCustomGpasRestTemplate(configProperties)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,6 +111,92 @@ class AppConfiguration {
|
|||||||
return AnonymizingGenerator()
|
return AnonymizingGenerator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@Bean
|
||||||
|
fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator {
|
||||||
|
try {
|
||||||
|
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
||||||
|
return GpasPseudonymGenerator(
|
||||||
|
configProperties,
|
||||||
|
retryTemplate,
|
||||||
|
createCustomGpasRestTemplate(configProperties)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCustomGpasRestTemplate(configProperties: GPasConfigProperties): RestTemplate {
|
||||||
|
fun getSslContext(certificateLocation: String): SSLContext? {
|
||||||
|
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||||
|
|
||||||
|
val fis = FileInputStream(certificateLocation)
|
||||||
|
val ca = CertificateFactory.getInstance("X.509")
|
||||||
|
.generateCertificate(BufferedInputStream(fis)) as X509Certificate
|
||||||
|
|
||||||
|
ks.load(null, null)
|
||||||
|
ks.setCertificateEntry(1.toString(), ca)
|
||||||
|
|
||||||
|
val tmf = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm()
|
||||||
|
)
|
||||||
|
tmf.init(ks)
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(null, tmf.trustManagers, null)
|
||||||
|
|
||||||
|
return sslContext
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCustomRestTemplate(customSslContext: SSLContext): RestTemplate {
|
||||||
|
val sslsf = SSLConnectionSocketFactory(customSslContext)
|
||||||
|
val socketFactoryRegistry = RegistryBuilder.create<ConnectionSocketFactory>()
|
||||||
|
.register("https", sslsf).register("http", PlainConnectionSocketFactory()).build()
|
||||||
|
|
||||||
|
val connectionManager = BasicHttpClientConnectionManager(
|
||||||
|
socketFactoryRegistry
|
||||||
|
)
|
||||||
|
val httpClient = HttpClients.custom()
|
||||||
|
.setConnectionManager(connectionManager).build()
|
||||||
|
|
||||||
|
val requestFactory = HttpComponentsClientHttpRequestFactory(
|
||||||
|
httpClient
|
||||||
|
)
|
||||||
|
return RestTemplate(requestFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!configProperties.sslCaLocation.isNullOrBlank()) {
|
||||||
|
val customSslContext = getSslContext(configProperties.sslCaLocation)
|
||||||
|
logger.warn(
|
||||||
|
String.format(
|
||||||
|
"%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.",
|
||||||
|
this.javaClass.name, configProperties.sslCaLocation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (customSslContext != null) {
|
||||||
|
return getCustomRestTemplate(customSslContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw RuntimeException("Custom SSL configuration for gPAS not usable")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "BUILDIN")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@Bean
|
||||||
|
fun buildinPseudonymGeneratorOnDeprecatedProperty(): Generator {
|
||||||
|
return AnonymizingGenerator()
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun pseudonymizeService(
|
fun pseudonymizeService(
|
||||||
generator: Generator,
|
generator: Generator,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -71,7 +71,7 @@ class AppKafkaConfiguration {
|
|||||||
kafkaProperties: KafkaProperties,
|
kafkaProperties: KafkaProperties,
|
||||||
kafkaResponseProcessor: KafkaResponseProcessor
|
kafkaResponseProcessor: KafkaResponseProcessor
|
||||||
): KafkaMessageListenerContainer<String, String> {
|
): KafkaMessageListenerContainer<String, String> {
|
||||||
val containerProperties = ContainerProperties(kafkaProperties.outputResponseTopic)
|
val containerProperties = ContainerProperties(kafkaProperties.responseTopic)
|
||||||
containerProperties.messageListener = kafkaResponseProcessor
|
containerProperties.messageListener = kafkaResponseProcessor
|
||||||
return KafkaMessageListenerContainer(consumerFactory, containerProperties)
|
return KafkaMessageListenerContainer(consumerFactory, containerProperties)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -87,14 +87,9 @@ class AppSecurityConfiguration(
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
||||||
fun filterChainOidc(
|
fun filterChainOidc(http: HttpSecurity, passwordEncoder: PasswordEncoder, userRoleRepository: UserRoleRepository, sessionRegistry: SessionRegistry): SecurityFilterChain {
|
||||||
http: HttpSecurity,
|
|
||||||
passwordEncoder: PasswordEncoder,
|
|
||||||
userRoleRepository: UserRoleRepository,
|
|
||||||
sessionRegistry: SessionRegistry
|
|
||||||
): SecurityFilterChain {
|
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeRequests {
|
||||||
authorize("/configs/**", hasRole("ADMIN"))
|
authorize("/configs/**", hasRole("ADMIN"))
|
||||||
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
|
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
|
||||||
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
|
||||||
@ -132,22 +127,13 @@ class AppSecurityConfiguration(
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true")
|
||||||
fun grantedAuthoritiesMapper(
|
fun grantedAuthoritiesMapper(userRoleRepository: UserRoleRepository, appSecurityConfigProperties: SecurityConfigProperties): GrantedAuthoritiesMapper {
|
||||||
userRoleRepository: UserRoleRepository,
|
|
||||||
appSecurityConfigProperties: SecurityConfigProperties
|
|
||||||
): GrantedAuthoritiesMapper {
|
|
||||||
return GrantedAuthoritiesMapper { grantedAuthority ->
|
return GrantedAuthoritiesMapper { grantedAuthority ->
|
||||||
grantedAuthority.filterIsInstance<OidcUserAuthority>()
|
grantedAuthority.filterIsInstance<OidcUserAuthority>()
|
||||||
.onEach {
|
.onEach {
|
||||||
val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername)
|
val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername)
|
||||||
if (userRole.isEmpty) {
|
if (userRole.isEmpty) {
|
||||||
userRoleRepository.save(
|
userRoleRepository.save(UserRole(null, it.userInfo.preferredUsername, appSecurityConfigProperties.defaultNewUserRole))
|
||||||
UserRole(
|
|
||||||
null,
|
|
||||||
it.userInfo.preferredUsername,
|
|
||||||
appSecurityConfigProperties.defaultNewUserRole
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
@ -161,7 +147,7 @@ class AppSecurityConfiguration(
|
|||||||
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true)
|
@ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true)
|
||||||
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
authorizeHttpRequests {
|
authorizeRequests {
|
||||||
authorize("/configs/**", hasRole("ADMIN"))
|
authorize("/configs/**", hasRole("ADMIN"))
|
||||||
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
|
authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
|
||||||
authorize("/report/**", hasRole("ADMIN"))
|
authorize("/report/**", hasRole("ADMIN"))
|
||||||
|
@ -1,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,13 +22,11 @@ package dev.dnpm.etl.processor.input
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.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(
|
||||||
@ -37,29 +35,10 @@ class KafkaInputListener(
|
|||||||
) : MessageListener<String, String> {
|
) : MessageListener<String, String> {
|
||||||
private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java)
|
private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java)
|
||||||
|
|
||||||
override fun onMessage(record: ConsumerRecord<String, String>) {
|
override fun onMessage(data: ConsumerRecord<String, String>) {
|
||||||
when (guessMimeType(record)) {
|
val mtbFile = objectMapper.readValue(data.value(), MtbFile::class.java)
|
||||||
MediaType.APPLICATION_JSON_VALUE -> handleBwhcMessage(record)
|
|
||||||
CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE -> handleDnpmV2Message(record)
|
|
||||||
else -> {
|
|
||||||
/* ignore other messages */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun guessMimeType(record: ConsumerRecord<String, String>): String {
|
|
||||||
if (record.headers().headers("contentType").toList().isEmpty()) {
|
|
||||||
// Fallback if no contentType set (old behavior)
|
|
||||||
return MediaType.APPLICATION_JSON_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
return record.headers().headers("contentType")?.firstOrNull()?.value().contentToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleBwhcMessage(record: ConsumerRecord<String, String>) {
|
|
||||||
val mtbFile = objectMapper.readValue(record.value(), MtbFile::class.java)
|
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
val firstRequestIdHeader = record.headers().headers("requestId")?.firstOrNull()
|
val firstRequestIdHeader = data.headers().headers("requestId")?.firstOrNull()
|
||||||
val requestId = if (null != firstRequestIdHeader) {
|
val requestId = if (null != firstRequestIdHeader) {
|
||||||
RequestId(String(firstRequestIdHeader.value()))
|
RequestId(String(firstRequestIdHeader.value()))
|
||||||
} else {
|
} else {
|
||||||
@ -82,10 +61,4 @@ class KafkaInputListener(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun handleDnpmV2Message(record: ConsumerRecord<String, String>) {
|
|
||||||
// Do not handle DNPM-V2 for now
|
|
||||||
logger.warn("Ignoring MTB File in DNPM V2 format: Not implemented yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -21,12 +21,9 @@ package dev.dnpm.etl.processor.input
|
|||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.dnpm.etl.processor.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.*
|
||||||
|
|
||||||
@ -43,26 +40,19 @@ class MtbFileRestController(
|
|||||||
return ResponseEntity.ok("Test")
|
return ResponseEntity.ok("Test")
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
|
@PostMapping
|
||||||
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
|
||||||
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
|
||||||
logger.debug("Accepted MTB File (bwHC V1) for processing")
|
logger.debug("Accepted MTB File for processing")
|
||||||
requestProcessor.processMtbFile(mtbFile)
|
requestProcessor.processMtbFile(mtbFile)
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Accepted MTB File (bwHC V1) and process deletion")
|
logger.debug("Accepted MTB File and process deletion")
|
||||||
val patientId = PatientId(mtbFile.patient.id)
|
val patientId = PatientId(mtbFile.patient.id)
|
||||||
requestProcessor.processDeletion(patientId)
|
requestProcessor.processDeletion(patientId)
|
||||||
}
|
}
|
||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] )
|
|
||||||
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
|
|
||||||
logger.debug("Accepted MTB File (DNPM V2) for processing")
|
|
||||||
requestProcessor.processMtbFile(mtbFile)
|
|
||||||
return ResponseEntity.accepted().build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping(path = ["{patientId}"])
|
@DeleteMapping(path = ["{patientId}"])
|
||||||
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
|
fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> {
|
||||||
logger.debug("Accepted patient ID to process deletion")
|
logger.debug("Accepted patient ID to process deletion")
|
||||||
@ -70,4 +60,4 @@ class MtbFileRestController(
|
|||||||
return ResponseEntity.accepted().build()
|
return ResponseEntity.accepted().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -22,12 +22,10 @@ package dev.dnpm.etl.processor.output
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.Consent
|
import de.ukw.ccc.bwhc.dto.Consent
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.apache.kafka.clients.producer.ProducerRecord
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.kafka.core.KafkaTemplate
|
||||||
import org.springframework.retry.support.RetryTemplate
|
import org.springframework.retry.support.RetryTemplate
|
||||||
|
|
||||||
@ -40,20 +38,14 @@ class KafkaMtbFileSender(
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java)
|
||||||
|
|
||||||
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val record =
|
val result = kafkaTemplate.send(
|
||||||
ProducerRecord(kafkaProperties.outputTopic, key(request), objectMapper.writeValueAsString(request))
|
kafkaProperties.topic,
|
||||||
when (request) {
|
key(request),
|
||||||
is BwhcV1MtbFileRequest -> record.headers()
|
objectMapper.writeValueAsString(Data(request.requestId, request.mtbFile))
|
||||||
.add("contentType", MediaType.APPLICATION_JSON_VALUE.toByteArray())
|
)
|
||||||
|
|
||||||
is DnpmV2MtbFileRequest -> record.headers()
|
|
||||||
.add("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = kafkaTemplate.send(record)
|
|
||||||
if (result.get() != null) {
|
if (result.get() != null) {
|
||||||
logger.debug("Sent file via KafkaMtbFileSender")
|
logger.debug("Sent file via KafkaMtbFileSender")
|
||||||
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
MtbFileSender.Response(RequestStatus.UNKNOWN)
|
||||||
@ -67,7 +59,7 @@ class KafkaMtbFileSender(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
||||||
val dummyMtbFile = MtbFile.builder()
|
val dummyMtbFile = MtbFile.builder()
|
||||||
.withConsent(
|
.withConsent(
|
||||||
Consent.builder()
|
Consent.builder()
|
||||||
@ -79,15 +71,12 @@ class KafkaMtbFileSender(
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val record =
|
val result = kafkaTemplate.send(
|
||||||
ProducerRecord(
|
kafkaProperties.topic,
|
||||||
kafkaProperties.outputTopic,
|
key(request),
|
||||||
key(request),
|
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 +91,16 @@ class KafkaMtbFileSender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun endpoint(): String {
|
override fun endpoint(): String {
|
||||||
return "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})"
|
return "${this.kafkaProperties.servers} (${this.kafkaProperties.topic}/${this.kafkaProperties.responseTopic})"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun key(request: MtbRequest): String {
|
private fun key(request: MtbFileSender.MtbFileRequest): String {
|
||||||
return when (request) {
|
return "{\"pid\": \"${request.mtbFile.patient.id}\"}"
|
||||||
is BwhcV1MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}"
|
|
||||||
is DnpmV2MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}"
|
|
||||||
is DeleteRequest -> "{\"pid\": \"${request.patientId.value}\"}"
|
|
||||||
else -> throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private fun key(request: MtbFileSender.DeleteRequest): String {
|
||||||
|
return "{\"pid\": \"${request.patientId.value}\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Data(val requestId: RequestId, val content: MtbFile)
|
||||||
|
}
|
@ -19,17 +19,25 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import org.springframework.http.HttpStatusCode
|
import org.springframework.http.HttpStatusCode
|
||||||
|
|
||||||
interface MtbFileSender {
|
interface MtbFileSender {
|
||||||
fun <T> send(request: MtbFileRequest<T>): Response
|
fun send(request: MtbFileRequest): Response
|
||||||
|
|
||||||
fun send(request: DeleteRequest): Response
|
fun send(request: DeleteRequest): Response
|
||||||
|
|
||||||
fun endpoint(): String
|
fun endpoint(): String
|
||||||
|
|
||||||
data class Response(val status: RequestStatus, val body: String = "")
|
data class Response(val status: RequestStatus, val body: String = "")
|
||||||
|
|
||||||
|
data class MtbFileRequest(val requestId: RequestId, val mtbFile: MtbFile)
|
||||||
|
|
||||||
|
data class DeleteRequest(val requestId: RequestId, val patientId: PatientPseudonym)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Int.asRequestStatus(): RequestStatus {
|
fun Int.asRequestStatus(): RequestStatus {
|
||||||
@ -43,4 +51,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,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,11 +19,10 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor.output
|
package dev.dnpm.etl.processor.output
|
||||||
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
import dev.dnpm.etl.processor.monitoring.asRequestStatus
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.HttpEntity
|
||||||
@ -47,11 +46,11 @@ abstract class RestMtbFileSender(
|
|||||||
|
|
||||||
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
abstract fun deleteUrl(patientId: PatientPseudonym): String
|
||||||
|
|
||||||
override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders(request)
|
val headers = getHttpHeaders()
|
||||||
val entityReq = HttpEntity(request.content, headers)
|
val entityReq = HttpEntity(request.mtbFile, headers)
|
||||||
val response = restTemplate.postForEntity(
|
val response = restTemplate.postForEntity(
|
||||||
sendUrl(),
|
sendUrl(),
|
||||||
entityReq,
|
entityReq,
|
||||||
@ -77,10 +76,10 @@ abstract class RestMtbFileSender(
|
|||||||
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(request: DeleteRequest): MtbFileSender.Response {
|
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
|
||||||
try {
|
try {
|
||||||
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
return retryTemplate.execute<MtbFileSender.Response, Exception> {
|
||||||
val headers = getHttpHeaders(request)
|
val headers = getHttpHeaders()
|
||||||
val entityReq = HttpEntity(null, headers)
|
val entityReq = HttpEntity(null, headers)
|
||||||
restTemplate.delete(
|
restTemplate.delete(
|
||||||
deleteUrl(request.patientId),
|
deleteUrl(request.patientId),
|
||||||
@ -103,15 +102,11 @@ abstract class RestMtbFileSender(
|
|||||||
return this.restTargetProperties.uri.orEmpty()
|
return this.restTargetProperties.uri.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getHttpHeaders(request: MtbRequest): HttpHeaders {
|
private fun getHttpHeaders(): HttpHeaders {
|
||||||
val username = restTargetProperties.username
|
val username = restTargetProperties.username
|
||||||
val password = restTargetProperties.password
|
val password = restTargetProperties.password
|
||||||
val headers = HttpHeaders()
|
val headers = HttpHeaders()
|
||||||
headers.contentType = when (request) {
|
headers.contentType = MediaType.APPLICATION_JSON
|
||||||
is BwhcV1MtbFileRequest -> MediaType.APPLICATION_JSON
|
|
||||||
is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
|
||||||
else -> MediaType.APPLICATION_JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
||||||
return headers
|
return headers
|
||||||
@ -121,4 +116,4 @@ abstract class RestMtbFileSender(
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -21,7 +21,6 @@ package dev.dnpm.etl.processor.pseudonym
|
|||||||
|
|
||||||
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.apache.commons.math3.random.RandomDataGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class AnonymizingGenerator : Generator {
|
class AnonymizingGenerator : Generator {
|
||||||
@ -32,9 +31,4 @@ class AnonymizingGenerator : Generator {
|
|||||||
.lowercase()
|
.lowercase()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun generateGenomDeTan(id: String?): String? {
|
|
||||||
val randomDataGenerator = RandomDataGenerator()
|
|
||||||
return randomDataGenerator.nextSecureHexString(64).lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -35,10 +35,6 @@ class PseudonymizeService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun genomDeTan(patientId: PatientId): String {
|
|
||||||
return generator.generateGenomDeTan(patientId.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prefix(): String {
|
fun prefix(): String {
|
||||||
return configProperties.prefix
|
return configProperties.prefix
|
||||||
}
|
}
|
||||||
|
@ -21,12 +21,12 @@ package dev.dnpm.etl.processor.pseudonym
|
|||||||
|
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.dnpm.etl.processor.PatientId
|
import dev.dnpm.etl.processor.PatientId
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils
|
import org.apache.commons.codec.digest.DigestUtils
|
||||||
|
|
||||||
/** 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) {
|
||||||
@ -49,11 +49,7 @@ infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
|||||||
}
|
}
|
||||||
this.lastGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
this.lastGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
||||||
this.molecularPathologyFindings?.forEach { it.patient = patientPseudonym }
|
this.molecularPathologyFindings?.forEach { it.patient = patientPseudonym }
|
||||||
this.molecularTherapies?.forEach { molecularTherapy ->
|
this.molecularTherapies?.forEach { molecularTherapy -> molecularTherapy.history.forEach { it.patient = patientPseudonym } }
|
||||||
molecularTherapy.history.forEach {
|
|
||||||
it.patient = patientPseudonym
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.ngsReports?.forEach { it.patient = patientPseudonym }
|
this.ngsReports?.forEach { it.patient = patientPseudonym }
|
||||||
this.previousGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
this.previousGuidelineTherapies?.forEach { it.patient = patientPseudonym }
|
||||||
this.rebiopsyRequests?.forEach { it.patient = patientPseudonym }
|
this.rebiopsyRequests?.forEach { it.patient = patientPseudonym }
|
||||||
@ -67,6 +63,7 @@ infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
|
|||||||
* Creates new hash of content IDs with given prefix except for patient IDs
|
* Creates new hash of content IDs with given prefix except for patient IDs
|
||||||
*
|
*
|
||||||
* @param pseudonymizeService The pseudonymizeService to be used
|
* @param pseudonymizeService The pseudonymizeService to be used
|
||||||
|
*
|
||||||
* @return The MTB file containing rehashed content IDs
|
* @return The MTB file containing rehashed content IDs
|
||||||
*/
|
*/
|
||||||
infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
infix fun MtbFile.anonymizeContentWith(pseudonymizeService: PseudonymizeService) {
|
||||||
@ -123,8 +120,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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,99 +223,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.familyMemberHistories?.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
|
|
||||||
infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService)
|
|
||||||
{
|
|
||||||
this.metadata.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id))
|
|
||||||
}
|
}
|
@ -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
|
||||||
@ -27,14 +27,10 @@ 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.addGenomDeTan
|
|
||||||
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.ModelProjectConsentPurpose
|
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
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.springframework.context.ApplicationEventPublisher
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
@ -59,47 +55,29 @@ class RequestProcessor(
|
|||||||
|
|
||||||
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
val pid = PatientId(mtbFile.patient.id)
|
||||||
|
|
||||||
mtbFile pseudonymizeWith pseudonymizeService
|
mtbFile pseudonymizeWith pseudonymizeService
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
mtbFile anonymizeContentWith pseudonymizeService
|
||||||
val request = BwhcV1MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
|
||||||
saveAndSend(request, pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: Mtb) {
|
val request = MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
||||||
processMtbFile(mtbFile, randomRequestId())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
|
val patientPseudonym = PatientPseudonym(request.mtbFile.patient.id)
|
||||||
val pid = PatientId(mtbFile.patient.id)
|
|
||||||
val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p ->
|
|
||||||
p.purpose == ModelProjectConsentPurpose.SEQUENCING
|
|
||||||
&& p.type == ConsentProvision.PERMIT
|
|
||||||
} == true
|
|
||||||
if (isModelProjectConsented) {
|
|
||||||
mtbFile addGenomDeTan pseudonymizeService
|
|
||||||
}
|
|
||||||
mtbFile pseudonymizeWith pseudonymizeService
|
|
||||||
mtbFile anonymizeContentWith pseudonymizeService
|
|
||||||
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
|
|
||||||
saveAndSend(request, pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
|
|
||||||
requestService.save(
|
requestService.save(
|
||||||
Request(
|
Request(
|
||||||
request.requestId,
|
requestId,
|
||||||
request.patientPseudonym(),
|
patientPseudonym,
|
||||||
pid,
|
pid,
|
||||||
fingerprint(request),
|
fingerprint(request.mtbFile),
|
||||||
RequestType.MTB_FILE,
|
RequestType.MTB_FILE,
|
||||||
RequestStatus.UNKNOWN
|
RequestStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (appConfigProperties.duplicationDetection && isDuplication(request)) {
|
if (appConfigProperties.duplicationDetection && isDuplication(mtbFile)) {
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
request.requestId,
|
requestId,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
RequestStatus.DUPLICATION
|
RequestStatus.DUPLICATION
|
||||||
)
|
)
|
||||||
@ -111,7 +89,7 @@ class RequestProcessor(
|
|||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
request.requestId,
|
requestId,
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
responseStatus.status,
|
responseStatus.status,
|
||||||
when (responseStatus.status) {
|
when (responseStatus.status) {
|
||||||
@ -122,22 +100,16 @@ class RequestProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean {
|
private fun isDuplication(pseudonymizedMtbFile: MtbFile): Boolean {
|
||||||
val patientPseudonym = when (pseudonymizedMtbFileRequest) {
|
val patientPseudonym = PatientPseudonym(pseudonymizedMtbFile.patient.id)
|
||||||
is BwhcV1MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
|
||||||
is DnpmV2MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastMtbFileRequestForPatient =
|
val lastMtbFileRequestForPatient =
|
||||||
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
|
||||||
val isLastRequestDeletion =
|
val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
||||||
requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
|
|
||||||
|
|
||||||
return null != lastMtbFileRequestForPatient
|
return null != lastMtbFileRequestForPatient
|
||||||
&& !isLastRequestDeletion
|
&& !isLastRequestDeletion
|
||||||
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(
|
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFile)
|
||||||
pseudonymizedMtbFileRequest
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun processDeletion(patientId: PatientId) {
|
fun processDeletion(patientId: PatientId) {
|
||||||
@ -159,7 +131,7 @@ class RequestProcessor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym))
|
val responseStatus = sender.send(MtbFileSender.DeleteRequest(requestId, patientPseudonym))
|
||||||
|
|
||||||
applicationEventPublisher.publishEvent(
|
applicationEventPublisher.publishEvent(
|
||||||
ResponseEvent(
|
ResponseEvent(
|
||||||
@ -188,11 +160,8 @@ class RequestProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint {
|
private fun fingerprint(mtbFile: MtbFile): Fingerprint {
|
||||||
return when (request) {
|
return fingerprint(objectMapper.writeValueAsString(mtbFile))
|
||||||
is BwhcV1MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content))
|
|
||||||
is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fingerprint(s: String): Fingerprint {
|
private fun fingerprint(s: String): Fingerprint {
|
||||||
@ -203,4 +172,4 @@ class RequestProcessor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -23,21 +23,10 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
|||||||
import com.jayway.jsonpath.JsonPath
|
import com.jayway.jsonpath.JsonPath
|
||||||
import com.jayway.jsonpath.PathNotFoundException
|
import com.jayway.jsonpath.PathNotFoundException
|
||||||
import de.ukw.ccc.bwhc.dto.MtbFile
|
import de.ukw.ccc.bwhc.dto.MtbFile
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
|
|
||||||
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) {
|
||||||
fun transform(mtbFile: MtbFile): MtbFile {
|
fun transform(mtbFile: MtbFile): MtbFile {
|
||||||
val json = transform(objectMapper.writeValueAsString(mtbFile))
|
var json = objectMapper.writeValueAsString(mtbFile)
|
||||||
return objectMapper.readValue(json, MtbFile::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun transform(mtbFile: Mtb): Mtb {
|
|
||||||
val json = transform(objectMapper.writeValueAsString(mtbFile))
|
|
||||||
return objectMapper.readValue(json, Mtb::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun transform(content: String): String {
|
|
||||||
var json = content
|
|
||||||
|
|
||||||
transformations.forEach { transformation ->
|
transformations.forEach { transformation ->
|
||||||
val jsonPath = JsonPath.parse(json)
|
val jsonPath = JsonPath.parse(json)
|
||||||
@ -59,7 +48,7 @@ class TransformationService(private val objectMapper: ObjectMapper, private val
|
|||||||
json = jsonPath.jsonString()
|
json = jsonPath.jsonString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return json
|
return objectMapper.readValue(json, MtbFile::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTransformations(): List<Transformation> {
|
fun getTransformations(): List<Transformation> {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is part of ETL-Processor
|
* This file is part of ETL-Processor
|
||||||
*
|
*
|
||||||
* Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
* Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package dev.dnpm.etl.processor
|
package dev.dnpm.etl.processor
|
||||||
|
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Fingerprint(val value: String) {
|
class Fingerprint(val value: String) {
|
||||||
@ -47,17 +46,4 @@ value class PatientId(val value: String)
|
|||||||
@JvmInline
|
@JvmInline
|
||||||
value class PatientPseudonym(val value: String)
|
value class PatientPseudonym(val value: String)
|
||||||
|
|
||||||
fun emptyPatientPseudonym() = PatientPseudonym("")
|
fun emptyPatientPseudonym() = PatientPseudonym("")
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom MediaTypes
|
|
||||||
*
|
|
||||||
* @since 0.11.0
|
|
||||||
*/
|
|
||||||
object CustomMediaType {
|
|
||||||
val APPLICATION_VND_DNPM_V2_MTB_JSON = MediaType("application", "vnd.dnpm.v2.mtb+json")
|
|
||||||
const val APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE = "application/vnd.dnpm.v2.mtb+json"
|
|
||||||
|
|
||||||
val APPLICATION_VND_DNPM_V2_RD_JSON = MediaType("application", "vnd.dnpm.v2.rd+json")
|
|
||||||
const val APPLICATION_VND_DNPM_V2_RD_JSON_VALUE = "application/vnd.dnpm.v2.rd+json"
|
|
||||||
}
|
|
@ -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,7 +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.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
|
||||||
@ -64,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
|
||||||
@ -84,15 +75,7 @@ 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(anyValueClass())
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass())
|
||||||
}
|
}
|
||||||
@ -106,22 +89,10 @@ class KafkaInputListenerTest {
|
|||||||
|
|
||||||
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(
|
||||||
ConsumerRecord(
|
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
|
||||||
"testtopic",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
-1L,
|
|
||||||
TimestampType.NO_TIMESTAMP_TYPE,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
"",
|
|
||||||
this.objectMapper.writeValueAsString(mtbFile),
|
|
||||||
headers,
|
|
||||||
Optional.empty()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>(), anyValueClass())
|
verify(requestProcessor, times(1)).processMtbFile(any(), anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -133,52 +104,9 @@ class KafkaInputListenerTest {
|
|||||||
|
|
||||||
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray())))
|
||||||
kafkaInputListener.onMessage(
|
kafkaInputListener.onMessage(
|
||||||
ConsumerRecord(
|
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
|
||||||
"testtopic",
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
-1L,
|
|
||||||
TimestampType.NO_TIMESTAMP_TYPE,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
"",
|
|
||||||
this.objectMapper.writeValueAsString(mtbFile),
|
|
||||||
headers,
|
|
||||||
Optional.empty()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass())
|
verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass())
|
||||||
}
|
}
|
||||||
|
|
||||||
@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())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -21,9 +21,7 @@ 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.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.services.RequestProcessor
|
import dev.dnpm.etl.processor.services.RequestProcessor
|
||||||
import dev.pcvolkmer.mv64e.mtb.Mtb
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -34,7 +32,6 @@ import org.mockito.Mockito.verify
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.any
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.anyValueClass
|
import org.mockito.kotlin.anyValueClass
|
||||||
import org.springframework.core.io.ClassPathResource
|
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
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
|
||||||
@ -73,7 +70,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -129,7 +126,7 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
|
verify(requestProcessor, times(1)).processMtbFile(any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -158,40 +155,6 @@ class MtbFileRestControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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)
|
|
||||||
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldRespondPostRequest() {
|
|
||||||
val mtbFileContent = ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8)
|
|
||||||
|
|
||||||
mockMvc.post("/mtb") {
|
|
||||||
content = mtbFileContent
|
|
||||||
contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON
|
|
||||||
}.andExpect {
|
|
||||||
status {
|
|
||||||
isAccepted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder()
|
fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
|
@ -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,166 @@ package dev.dnpm.etl.processor.output
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.KafkaProperties
|
import dev.dnpm.etl.processor.config.KafkaProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
|
||||||
import org.apache.kafka.clients.producer.ProducerRecord
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.junit.jupiter.MockitoExtension
|
import org.mockito.junit.jupiter.MockitoExtension
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.*
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.kafka.core.KafkaTemplate
|
||||||
import org.springframework.kafka.support.SendResult
|
import org.springframework.kafka.support.SendResult
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletableFuture.completedFuture
|
import java.util.concurrent.CompletableFuture.completedFuture
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension::class)
|
@ExtendWith(MockitoExtension::class)
|
||||||
class KafkaMtbFileSenderTest {
|
class KafkaMtbFileSenderTest {
|
||||||
|
|
||||||
@Nested
|
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(TEST_REQUEST_ID, 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(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
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(TEST_REQUEST_ID, 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(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldSendDeleteRequestWithCorrectKeyAndBody() {
|
||||||
|
doAnswer {
|
||||||
|
completedFuture(SendResult<String, String>(null, null))
|
||||||
|
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
|
||||||
|
|
||||||
|
kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
|
||||||
|
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(TEST_REQUEST_ID, 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(TEST_REQUEST_ID, 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(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(anyString(), anyString(), anyString())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
fun bwhcV1MtbFile(consentStatus: Consent.Status): MtbFile {
|
fun mtbFile(consentStatus: Consent.Status): MtbFile {
|
||||||
return if (consentStatus == Consent.Status.ACTIVE) {
|
return if (consentStatus == Consent.Status.ACTIVE) {
|
||||||
MtbFile.builder()
|
MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -311,35 +215,8 @@ class KafkaMtbFileSenderTest {
|
|||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dnpmV2MtbFile(): Mtb {
|
fun kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): KafkaMtbFileSender.Data {
|
||||||
return Mtb().apply {
|
return KafkaMtbFileSender.Data(requestId, mtbFile(consentStatus))
|
||||||
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
this.birthDate = Date.from(Instant.now())
|
|
||||||
this.gender = GenderCoding().apply {
|
|
||||||
this.code = GenderCodingCode.MALE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.episodesOfCare = listOf(
|
|
||||||
MtbEpisodeOfCare().apply {
|
|
||||||
this.id = "1"
|
|
||||||
this.patient = Reference().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
}
|
|
||||||
this.period = PeriodDate().apply {
|
|
||||||
this.start = Date.from(Instant.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bwhcV1kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): MtbRequest {
|
|
||||||
return BwhcV1MtbFileRequest(requestId, bwhcV1MtbFile(consentStatus))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dnmpV2kafkaRecordData(requestId: RequestId): MtbRequest {
|
|
||||||
return DnpmV2MtbFileRequest(requestId, dnpmV2MtbFile())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null)
|
||||||
@ -354,4 +231,4 @@ class KafkaMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -30,16 +30,16 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
|
|||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
import org.springframework.http.HttpHeaders
|
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
import org.springframework.test.web.client.ExpectedCount
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -84,12 +84,11 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
this.mockRestServiceServer
|
this.mockRestServiceServer
|
||||||
.expect(method(HttpMethod.POST))
|
.expect(method(HttpMethod.POST))
|
||||||
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
.andExpect(requestTo("http://localhost:9000/mtbfile/MTBFile"))
|
||||||
.andExpect(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))
|
|
||||||
.andRespond {
|
.andRespond {
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -119,7 +118,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(BwhcV1MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -149,7 +148,7 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
@ -310,4 +309,4 @@ class RestBwhcMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -22,8 +22,6 @@ package dev.dnpm.etl.processor.output
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
|
||||||
import dev.dnpm.etl.processor.CustomMediaType
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
import dev.dnpm.etl.processor.PatientPseudonym
|
||||||
import dev.dnpm.etl.processor.RequestId
|
import dev.dnpm.etl.processor.RequestId
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
@ -31,208 +29,136 @@ import dev.dnpm.etl.processor.config.AppConfiguration
|
|||||||
import dev.dnpm.etl.processor.config.RestTargetProperties
|
import dev.dnpm.etl.processor.config.RestTargetProperties
|
||||||
import dev.dnpm.etl.processor.monitoring.ReportService
|
import dev.dnpm.etl.processor.monitoring.ReportService
|
||||||
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
import dev.pcvolkmer.mv64e.mtb.*
|
import dev.dnpm.etl.processor.output.RestBwhcMtbFileSenderTest.Companion
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
import org.springframework.http.HttpHeaders
|
|
||||||
import org.springframework.http.HttpMethod
|
import org.springframework.http.HttpMethod
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.retry.backoff.NoBackOffPolicy
|
import org.springframework.retry.backoff.NoBackOffPolicy
|
||||||
import org.springframework.retry.policy.SimpleRetryPolicy
|
import org.springframework.retry.policy.SimpleRetryPolicy
|
||||||
import org.springframework.retry.support.RetryTemplateBuilder
|
import org.springframework.retry.support.RetryTemplateBuilder
|
||||||
import org.springframework.test.web.client.ExpectedCount
|
import org.springframework.test.web.client.ExpectedCount
|
||||||
import org.springframework.test.web.client.MockRestServiceServer
|
import org.springframework.test.web.client.MockRestServiceServer
|
||||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
||||||
|
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
||||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class RestDipMtbFileSenderTest {
|
class RestDipMtbFileSenderTest {
|
||||||
|
|
||||||
@Nested
|
private lateinit var mockRestServiceServer: MockRestServiceServer
|
||||||
inner class BwhcV1ContentRequest {
|
|
||||||
|
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
private lateinit var restMtbFileSender: RestMtbFileSender
|
||||||
|
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
@BeforeEach
|
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@ParameterizedTest
|
||||||
inner class DnpmV2ContentRequest {
|
@MethodSource("deleteRequestWithResponseSource")
|
||||||
|
fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) {
|
||||||
private lateinit var mockRestServiceServer: MockRestServiceServer
|
this.mockRestServiceServer
|
||||||
|
.expect(method(HttpMethod.DELETE))
|
||||||
private lateinit var restMtbFileSender: RestMtbFileSender
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
||||||
|
.andRespond {
|
||||||
private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build()))
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
|
||||||
@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
|
val response = restMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
.expect(expectedCount, method(HttpMethod.DELETE))
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}"))
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
.andRespond {
|
}
|
||||||
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
@ParameterizedTest
|
||||||
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
@MethodSource("mtbFileRequestWithResponseSource")
|
||||||
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
fun shouldReturnExpectedResponseForMtbFilePost(requestWithResponse: RequestWithResponse) {
|
||||||
|
this.mockRestServiceServer
|
||||||
|
.expect(method(HttpMethod.POST))
|
||||||
|
.andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record"))
|
||||||
|
.andRespond {
|
||||||
|
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("mtbFileRequestWithResponseSource")
|
||||||
|
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
|
||||||
|
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(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("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(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
|
||||||
|
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
|
||||||
|
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -245,7 +171,7 @@ class RestDipMtbFileSenderTest {
|
|||||||
val TEST_REQUEST_ID = RequestId("TestId")
|
val TEST_REQUEST_ID = RequestId("TestId")
|
||||||
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
|
||||||
|
|
||||||
val bwhcV1mtbFile: MtbFile = MtbFile.builder()
|
val mtbFile: MtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
Patient.builder()
|
Patient.builder()
|
||||||
.withId("PID")
|
.withId("PID")
|
||||||
@ -269,29 +195,6 @@ class RestDipMtbFileSenderTest {
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun dnpmV2MtbFile(): Mtb {
|
|
||||||
return Mtb().apply {
|
|
||||||
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
this.birthDate = Date.from(Instant.now())
|
|
||||||
this.gender = GenderCoding().apply {
|
|
||||||
this.code = GenderCodingCode.MALE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.episodesOfCare = listOf(
|
|
||||||
MtbEpisodeOfCare().apply {
|
|
||||||
this.id = "1"
|
|
||||||
this.patient = Reference().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
}
|
|
||||||
this.period = PeriodDate().apply {
|
|
||||||
this.start = Date.from(Instant.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -408,4 +311,4 @@ class RestDipMtbFileSenderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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,10 +21,7 @@ package dev.dnpm.etl.processor.pseudonym
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import de.ukw.ccc.bwhc.dto.Patient
|
|
||||||
import dev.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
|
||||||
@ -34,242 +31,168 @@ 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 {
|
||||||
|
|
||||||
@Nested
|
private fun fakeMtbFile(): MtbFile {
|
||||||
inner class UsingBwhcDatamodel {
|
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
||||||
|
return ObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
||||||
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 ObjectMapper().readValue(mtbFile, MtbFile::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MtbFile.serialized(): String {
|
|
||||||
return ObjectMapper().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
|
private fun MtbFile.serialized(): String {
|
||||||
inner class UsingDnpmV2Datamodel {
|
return ObjectMapper().writeValueAsString(this)
|
||||||
|
|
||||||
val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json"
|
|
||||||
val CLEAN_PATIENT_ID = "644bae7a-56f6-4ee8-b02f-c532e65af5b1"
|
|
||||||
|
|
||||||
private fun fakeMtbFile(): Mtb {
|
|
||||||
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
|
|
||||||
return ObjectMapper().readValue(mtbFile, Mtb::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Mtb.serialized(): String {
|
|
||||||
return ObjectMapper().writeValueAsString(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
val mtbFile = fakeMtbFile()
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
|
|
||||||
assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID")
|
|
||||||
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
|
|
||||||
doAnswer {
|
|
||||||
it.arguments[0]
|
|
||||||
"PSEUDO-ID"
|
|
||||||
}.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
|
|
||||||
|
|
||||||
doAnswer {
|
|
||||||
"TESTDOMAIN"
|
|
||||||
}.whenever(pseudonymizeService).prefix()
|
|
||||||
|
|
||||||
val mtbFile = Mtb().apply {
|
|
||||||
this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
this.birthDate = Date.from(Instant.now())
|
|
||||||
this.gender = GenderCoding().apply {
|
|
||||||
this.code = GenderCodingCode.MALE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.episodesOfCare = listOf(
|
|
||||||
MtbEpisodeOfCare().apply {
|
|
||||||
this.id = "1"
|
|
||||||
this.patient = Reference().apply {
|
|
||||||
this.id = "PID"
|
|
||||||
}
|
|
||||||
this.period = PeriodDate().apply {
|
|
||||||
this.start = Date.from(Instant.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
mtbFile.pseudonymizeWith(pseudonymizeService)
|
|
||||||
mtbFile.anonymizeContentWith(pseudonymizeService)
|
|
||||||
|
|
||||||
assertThat(mtbFile.episodesOfCare).hasSize(1)
|
|
||||||
assertThat(mtbFile.episodesOfCare.map { it.id }).isNotNull
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -72,7 +72,7 @@ class PseudonymizeServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sanitizeFileName(@Mock generator: GpasPseudonymGenerator) {
|
fun sanitizeFileName(@Mock generator: GpasPseudonymGenerator) {
|
||||||
val result = GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
|
val result= GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
|
||||||
|
|
||||||
assertThat(result).isEqualTo("l___a_bs_1_2_3_")
|
assertThat(result).isEqualTo("l___a_bs_1_2_3_")
|
||||||
}
|
}
|
||||||
@ -90,10 +90,4 @@ class PseudonymizeServiceTest {
|
|||||||
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
|
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldReturnDifferentValues() {
|
|
||||||
val ag = AnonymizingGenerator()
|
|
||||||
val tan = ag.generateGenomDeTan("12345")
|
|
||||||
assertThat(tan).hasSize(64)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -21,19 +21,14 @@ package dev.dnpm.etl.processor.services
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.ukw.ccc.bwhc.dto.*
|
import de.ukw.ccc.bwhc.dto.*
|
||||||
import dev.dnpm.etl.processor.Fingerprint
|
import dev.dnpm.etl.processor.*
|
||||||
import dev.dnpm.etl.processor.PatientId
|
|
||||||
import dev.dnpm.etl.processor.PatientPseudonym
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfigProperties
|
import dev.dnpm.etl.processor.config.AppConfigProperties
|
||||||
import dev.dnpm.etl.processor.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
|
||||||
@ -114,7 +109,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -173,7 +168,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -228,7 +223,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@ -236,7 +231,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -291,7 +286,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0] as String
|
it.arguments[0] as String
|
||||||
@ -299,7 +294,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -341,7 +336,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
@ -359,7 +354,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
@ -377,7 +372,7 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.ERROR)
|
MtbFileSender.Response(status = RequestStatus.ERROR)
|
||||||
}.whenever(sender).send(any<DeleteRequest>())
|
}.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
|
||||||
|
|
||||||
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
this.requestProcessor.processDeletion(TEST_PATIENT_ID)
|
||||||
|
|
||||||
@ -409,11 +404,11 @@ class RequestProcessorTest {
|
|||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
it.arguments[0]
|
it.arguments[0]
|
||||||
}.whenever(transformationService).transform(any<MtbFile>())
|
}.whenever(transformationService).transform(any())
|
||||||
|
|
||||||
doAnswer {
|
doAnswer {
|
||||||
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
MtbFileSender.Response(status = RequestStatus.SUCCESS)
|
||||||
}.whenever(sender).send(any<BwhcV1MtbFileRequest>())
|
}.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
|
||||||
|
|
||||||
val mtbFile = MtbFile.builder()
|
val mtbFile = MtbFile.builder()
|
||||||
.withPatient(
|
.withPatient(
|
||||||
@ -451,4 +446,4 @@ class RequestProcessorTest {
|
|||||||
val TEST_PATIENT_ID = PatientId("TEST_12345678901")
|
val TEST_PATIENT_ID = PatientId("TEST_12345678901")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user