summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md24
-rw-r--r--build.gradle.kts41
-rw-r--r--deploy/docker-compose.yaml2
-rw-r--r--deploy/env-sample.env2
-rw-r--r--dev-compose.yml6
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt73
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt4
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt30
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt76
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt75
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt136
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt63
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt273
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt287
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt88
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt73
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt312
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java122
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt120
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt25
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt6
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt11
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt6
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt114
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt48
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt7
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt6
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt20
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt8
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt (renamed from src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt)2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt (renamed from src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt)5
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt61
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt35
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt10
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/types.kt49
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt16
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt19
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt14
-rw-r--r--src/main/resources/application-dev.yml35
-rw-r--r--src/main/resources/db/migration/mariadb/V0_4_0__RenamePatientPseudonym.sql1
-rw-r--r--src/main/resources/db/migration/postgresql/V0_4_0__RenamePatientPseudonym.sql1
-rw-r--r--src/main/resources/static/style.css10
-rw-r--r--src/main/resources/templates/configs/gPasConnectionAvailable.html15
-rw-r--r--src/main/resources/templates/configs/outputConnectionAvailable.html42
-rw-r--r--src/main/resources/templates/index.html32
-rw-r--r--src/main/resources/templates/report.html2
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/helpers.kt29
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt8
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt11
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt23
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSenderTest.kt19
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt10
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt (renamed from src/test/kotlin/dev/dnpm/etl/processor/services/TokenServiceTest.kt)15
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt202
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt125
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt177
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt37
62 files changed, 2498 insertions, 578 deletions
diff --git a/README.md b/README.md
index 5979219..45efef9 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,15 @@ Siehe hierzu auch: https://github.com/CCC-MF/kafka-to-bwhc
## Konfiguration
+### Breaking Changes nach Version 0.10
+
+In Versionen des ETL-Processors **nach Version 0.10** werden die folgenden Konfigurationsoptionen entfernt:
+
+* `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`: Nutzen Sie hier, wie unter [_Integration eines eigenen Root CA
+ Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben, das Einbinden eigener Zertifikate.
+* `APP_KAFKA_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_TOPIC`
+* `APP_KAFKA_RESPONSE_TOPIC`: Nutzen Sie nun die Konfigurationsoption `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`
+
### Pseudonymisierung der Patienten-ID
Wenn eine URI zu einer gPAS-Instanz (Version >= 2023.1.0) angegeben ist, wird diese verwendet.
@@ -64,9 +73,12 @@ Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfiguri
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
* ~~`APP_PSEUDONYMIZE_GPAS_SSLCALOCATION`~~: **Veraltet** - Root Zertifikat für gPas, falls es dediziert hinzugefügt werden muss.
+ **Wird in nach Version 0.10 entfernt**
-Der Konfigurationsparameter `APP_PSEUDONYMIZE_GPAS_SSLCALOCATION` sollte nicht mehr verwendet werden und wird in einer kommenden Version entfernt.
-Stattdessen sollte das Root Zertifikat wie unter [_Integration eines eigenen Root CA Zertifikats_](#integration-eines-eigenen-root-ca-zertifikats) beschrieben eingebunden werden.
+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
@@ -157,6 +169,8 @@ https://testonkostar:MTg1NTL...NGU4@etl.example.com/mtbfile
Ist die Verwendung von Tokens aktiv, werden Anfragen ohne die Angabe der Token-Information abgelehnt.
+Alternativ kann eine Authentifizierung über Benutzername/Passwort oder OIDC erfolgen.
+
### Transformation von Werten
In Onkostar kann es vorkommen, dass ein Wert eines Merkmalskatalogs an einem Standort angepasst wurde und dadurch nicht dem Wert entspricht,
@@ -183,15 +197,17 @@ 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 das bwHC-Backend gesendet wird:
* `APP_REST_URI`: URI der zu benutzenden API der bwHC-Backend-Instanz. z.B.: `http://localhost:9000/bwhc/etl/api`
+* `APP_REST_USERNAME`: Basic-Auth-Benutzername für bwHC-Backend
+* `APP_REST_PASSWORD`: Basic-Auth-Passwort für bwHC-Backend
#### Kafka-Topics
Folgende Umgebungsvariablen müssen gesetzt sein, damit ein bwHC-MTB-File an ein Kafka-Topic übermittelt wird:
* `APP_KAFKA_OUTPUT_TOPIC`: Zu verwendendes Topic zum Versenden von Anfragen.
- Ersetzt in einer kommenden Version `APP_KAFKA_TOPIC`.
+ Ersetzt ~~`APP_KAFKA_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
* `APP_KAFKA_OUTPUT_RESPONSE_TOPIC`: Topic mit Antworten über den Erfolg des Versendens. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_response".
- Ersetzt in einer kommenden Version `APP_KAFKA_RESPONSE_TOPIC`.
+ Ersetzt ~~`APP_KAFKA_RESPONSE_TOPIC`~~, **welches nach Version 0.10 entfernt wird**.
* `APP_KAFKA_GROUP_ID`: Kafka GroupID des Consumers. Standardwert: `APP_KAFKA_TOPIC` mit Anhang "_group".
* `APP_KAFKA_SERVERS`: Zu verwendende Kafka-Bootstrap-Server als kommagetrennte Liste
diff --git a/build.gradle.kts b/build.gradle.kts
index 011ade4..4e6499b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,23 +1,26 @@
import org.gradle.api.tasks.testing.logging.TestLogEvent
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
war
- id("org.springframework.boot") version "3.2.11"
- id("io.spring.dependency-management") version "1.1.5"
+ id("org.springframework.boot") version "3.3.5"
+ id("io.spring.dependency-management") version "1.1.6"
kotlin("jvm") version "1.9.24"
kotlin("plugin.spring") version "1.9.24"
+ jacoco
}
group = "dev.dnpm"
-version = "0.9-SNAPSHOT"
+version = "0.10.0-SNAPSHOT"
var versions = mapOf(
"bwhc-dto-java" to "0.3.0",
"hapi-fhir" to "6.10.5",
- "httpclient5" to "5.2.3",
+ "commons-compress" to "1.26.2",
"mockito-kotlin" to "5.3.1",
+ "archunit" to "1.3.0",
// Webjars
"echarts" to "5.4.3",
"htmx.org" to "1.9.12"
@@ -62,13 +65,14 @@ dependencies {
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.kafka:spring-kafka")
+ implementation("org.flywaydb:flyway-database-postgresql")
implementation("org.flywaydb:flyway-mysql")
implementation("commons-codec:commons-codec")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("de.ukw.ccc:bwhc-dto-java:${versions["bwhc-dto-java"]}")
implementation("ca.uhn.hapi.fhir:hapi-fhir-base:${versions["hapi-fhir"]}")
implementation("ca.uhn.hapi.fhir:hapi-fhir-structures-r4:${versions["hapi-fhir"]}")
- implementation("org.apache.httpcomponents.client5:httpclient5:${versions["httpclient5"]}")
+ implementation("org.apache.httpcomponents.client5:httpclient5")
implementation("com.jayway.jsonpath:json-path")
implementation("org.webjars:webjars-locator:0.52")
implementation("org.webjars.npm:echarts:${versions["echarts"]}")
@@ -85,14 +89,17 @@ dependencies {
testImplementation("org.mockito.kotlin:mockito-kotlin:${versions["mockito-kotlin"]}")
integrationTestImplementation("org.testcontainers:junit-jupiter")
integrationTestImplementation("org.testcontainers:postgresql")
+ integrationTestImplementation("com.tngtech.archunit:archunit:${versions["archunit"]}")
+ integrationTestImplementation("net.sourceforge.htmlunit:htmlunit")
+ 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:1.26.1")
+ integrationTestImplementation("org.apache.commons:commons-compress:${versions["commons-compress"]}")
}
tasks.withType<KotlinCompile> {
- kotlinOptions {
- freeCompilerArgs += "-Xjsr305=strict"
- jvmTarget = "21"
+ compilerOptions {
+ freeCompilerArgs.add("-Xjsr305=strict")
+ jvmTarget.set(JvmTarget.JVM_21)
}
}
@@ -112,8 +119,22 @@ task<Test>("integrationTest") {
shouldRunAfter("test")
}
+tasks.register("allTests") {
+ dependsOn(tasks.withType<Test>())
+}
+
+tasks.jacocoTestReport {
+ dependsOn("allTests")
+
+ executionData(fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec"))
+
+ reports {
+ xml.required = true
+ }
+}
+
tasks.named<BootBuildImage>("bootBuildImage") {
- imageName.set("ghcr.io/ccc-mf/etl-processor")
+ imageName.set("ghcr.io/pcvolkmer/etl-processor")
// Binding for CA Certs
bindings.set(listOf(
diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml
index 4641ca6..2180786 100644
--- a/deploy/docker-compose.yaml
+++ b/deploy/docker-compose.yaml
@@ -18,6 +18,8 @@ services:
APP_KAFKA_GROUP_ID: ${DNPM_KAFKA_GROUP_ID}
APP_KAFKA_RESPONSE_TOPIC: ${DNPM_KAFKA_RESPONSE_TOPIC}
APP_REST_URI: ${DNPM_BWHC_REST_URI}
+ APP_REST_USERNAME: ${DNPM_BWHC_REST_USERNAME}
+ APP_REST_PASSWORD: ${DNPM_BWHC_REST_PASSWORD}
APP_SECURITY_ADMIN_USER: ${DNPM_ADMIN_USER}
APP_SECURITY_ADMIN_PASSWORD: ${DNPM_ADMIN_PASSWORD}
SPRING_DATASOURCE_URL: ${DNPM_DATASOURCE_URL}
diff --git a/deploy/env-sample.env b/deploy/env-sample.env
index 04a3f8f..9c06341 100644
--- a/deploy/env-sample.env
+++ b/deploy/env-sample.env
@@ -28,6 +28,8 @@ DNPM_DATASOURCE_URL=jdbc:mariadb://dnpm-monitor-db:3306/$DNPM_MARIADB_DB
## TARGET SYSTEMS CONFIG
# in case of direct access to bwhc enter endpoint url here
DNPM_BWHC_REST_URI=
+DNPM_BWHC_REST_USERNAME=
+DNPM_BWHC_REST_PASSWORD=
# produce mtb files to this topic - values 'false' disabling kafka processing
DNPM_KAFKA_TOPIC=false
diff --git a/dev-compose.yml b/dev-compose.yml
index a162fcc..e2dfdb6 100644
--- a/dev-compose.yml
+++ b/dev-compose.yml
@@ -17,8 +17,9 @@ services:
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
+ ## Use AKHQ as Kafka web frontend
akhq:
- image: tchiotludo/akhq:0.21.0
+ image: tchiotludo/akhq:0.25.0
environment:
AKHQ_CONFIGURATION: |
akhq:
@@ -32,6 +33,8 @@ services:
ports:
- "8084:8080"
+
+## For use with MariaDB
mariadb:
image: mariadb:10
ports:
@@ -42,6 +45,7 @@ services:
MARIADB_PASSWORD: dev
MARIADB_ROOT_PASSWORD: dev
+## For use with Postgres
# postgres:
# image: postgres:alpine
# ports:
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 509c4a2..20db9ad 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt
new file mode 100644
index 0000000..308d0cc
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt
@@ -0,0 +1,73 @@
+package dev.dnpm.etl.processor
+
+import com.tngtech.archunit.core.domain.JavaClasses
+import com.tngtech.archunit.core.importer.ClassFileImporter
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.data.repository.Repository
+
+class EtlProcessorArchTest {
+
+ private lateinit var noTestClasses: JavaClasses
+
+ @BeforeEach
+ fun setUp() {
+ this.noTestClasses = ClassFileImporter()
+ .withImportOption { !(it.contains("/test/") || it.contains("/integrationTest/")) }
+ .importPackages("dev.dnpm.etl.processor")
+ }
+
+ @Test
+ fun noClassesInInputPackageShouldDependOnMonitoringPackage() {
+ val rule = noClasses()
+ .that()
+ .resideInAPackage("..input")
+ .should().dependOnClassesThat()
+ .resideInAnyPackage("..monitoring")
+
+ rule.check(noTestClasses)
+ }
+
+ @Test
+ fun noClassesInInputPackageShouldDependOnRepositories() {
+ val rule = noClasses()
+ .that()
+ .resideInAPackage("..input")
+ .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
+
+ rule.check(noTestClasses)
+ }
+
+ @Test
+ fun noClassesInOutputPackageShouldDependOnRepositories() {
+ val rule = noClasses()
+ .that()
+ .resideInAPackage("..output")
+ .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
+
+ rule.check(noTestClasses)
+ }
+
+ @Test
+ fun noClassesInWebPackageShouldDependOnRepositories() {
+ val rule = noClasses()
+ .that()
+ .resideInAPackage("..web")
+ .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository")
+
+ rule.check(noTestClasses)
+ }
+
+ @Test
+ fun repositoryClassNamesShouldEndWithRepository() {
+ val rule = classes()
+ .that()
+ .areInterfaces().and().areAssignableTo(Repository::class.java)
+ .should().haveSimpleNameEndingWith("Repository")
+
+ rule.check(noTestClasses)
+ }
+
+} \ No newline at end of file
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt
index 262aca0..c7454ed 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt
@@ -27,8 +27,8 @@ import dev.dnpm.etl.processor.output.RestMtbFileSender
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
import dev.dnpm.etl.processor.services.RequestProcessor
-import dev.dnpm.etl.processor.services.TokenRepository
-import dev.dnpm.etl.processor.services.TokenService
+import dev.dnpm.etl.processor.security.TokenRepository
+import dev.dnpm.etl.processor.security.TokenService
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt
new file mode 100644
index 0000000..6ca420f
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt
@@ -0,0 +1,30 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor
+
+import org.mockito.ArgumentMatchers
+
+@Suppress("UNCHECKED_CAST")
+inline fun <reified T> anyValueClass(): T {
+ val unboxedClass = T::class.java.declaredFields.first().type
+ return ArgumentMatchers.any(unboxedClass as Class<T>)
+ ?: T::class.java.getDeclaredMethod("box-impl", unboxedClass)
+ .invoke(null, null) as T
+} \ No newline at end of file
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
index f1586d0..670020f 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
@@ -21,13 +21,15 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
+import dev.dnpm.etl.processor.anyValueClass
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.security.TokenRepository
+import dev.dnpm.etl.processor.security.UserRoleRepository
import dev.dnpm.etl.processor.services.RequestProcessor
-import dev.dnpm.etl.processor.services.TokenRepository
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import org.mockito.ArgumentMatchers.anyString
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.mockito.kotlin.never
@@ -37,6 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired
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.security.oauth2.client.registration.ClientRegistrationRepository
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.context.ContextConfiguration
@@ -92,6 +95,19 @@ class MtbFileRestControllerTest {
}
@Test
+ fun testShouldGrantPermissionToSendMtbFileToAdminUser() {
+ mockMvc.post("/mtbfile") {
+ with(user("onkostarserver").roles("ADMIN"))
+ contentType = MediaType.APPLICATION_JSON
+ content = ObjectMapper().writeValueAsString(mtbFile)
+ }.andExpect {
+ status { isAccepted() }
+ }
+
+ verify(requestProcessor, times(1)).processMtbFile(any())
+ }
+
+ @Test
fun testShouldDenyPermissionToSendMtbFile() {
mockMvc.post("/mtbfile") {
with(anonymous())
@@ -105,6 +121,19 @@ class MtbFileRestControllerTest {
}
@Test
+ fun testShouldDenyPermissionToSendMtbFileForUser() {
+ mockMvc.post("/mtbfile") {
+ with(user("fakeuser").roles("USER"))
+ contentType = MediaType.APPLICATION_JSON
+ content = ObjectMapper().writeValueAsString(mtbFile)
+ }.andExpect {
+ status { isForbidden() }
+ }
+
+ verify(requestProcessor, never()).processMtbFile(any())
+ }
+
+ @Test
fun testShouldGrantPermissionToDeletePatientData() {
mockMvc.delete("/mtbfile/12345678") {
with(user("onkostarserver").roles("MTBFILE"))
@@ -112,7 +141,7 @@ class MtbFileRestControllerTest {
status { isAccepted() }
}
- verify(requestProcessor, times(1)).processDeletion(anyString())
+ verify(requestProcessor, times(1)).processDeletion(anyValueClass())
}
@Test
@@ -123,7 +152,46 @@ class MtbFileRestControllerTest {
status { isUnauthorized() }
}
- verify(requestProcessor, never()).processDeletion(anyString())
+ verify(requestProcessor, never()).processDeletion(anyValueClass())
+ }
+
+ @Nested
+ @MockBean(UserRoleRepository::class, ClientRegistrationRepository::class)
+ @TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=BUILDIN",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret",
+ "app.security.enable-tokens=true",
+ "app.security.enable-oidc=true"
+ ]
+ )
+ inner class WithOidcEnabled {
+ @Test
+ fun testShouldGrantPermissionToSendMtbFileToAdminUser() {
+ mockMvc.post("/mtbfile") {
+ with(user("onkostarserver").roles("ADMIN"))
+ contentType = MediaType.APPLICATION_JSON
+ content = ObjectMapper().writeValueAsString(mtbFile)
+ }.andExpect {
+ status { isAccepted() }
+ }
+
+ verify(requestProcessor, times(1)).processMtbFile(any())
+ }
+
+ @Test
+ fun testShouldGrantPermissionToSendMtbFileToUser() {
+ mockMvc.post("/mtbfile") {
+ with(user("onkostarserver").roles("USER"))
+ contentType = MediaType.APPLICATION_JSON
+ content = ObjectMapper().writeValueAsString(mtbFile)
+ }.andExpect {
+ status { isAccepted() }
+ }
+
+ verify(requestProcessor, times(1)).processMtbFile(any())
+ }
}
companion object {
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt
new file mode 100644
index 0000000..bef124c
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt
@@ -0,0 +1,75 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.monitoring
+
+import dev.dnpm.etl.processor.*
+import dev.dnpm.etl.processor.output.MtbFileSender
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.transaction.annotation.Transactional
+import org.testcontainers.junit.jupiter.Testcontainers
+import java.time.Instant
+
+@Testcontainers
+@ExtendWith(SpringExtension::class)
+@DataJdbcTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Transactional
+@MockBean(MtbFileSender::class)
+@TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=buildin",
+ "app.rest.uri=http://example.com"
+ ]
+)
+class RequestRepositoryTest : AbstractTestcontainerTest() {
+
+ private lateinit var requestRepository: RequestRepository
+
+ @BeforeEach
+ fun setUp(
+ @Autowired requestRepository: RequestRepository
+ ) {
+ this.requestRepository = requestRepository
+ }
+
+ @Test
+ fun shouldSaveRequest() {
+ val request = Request(
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-07-07T00:00:00Z")
+ )
+
+ requestRepository.save(request)
+ }
+
+} \ No newline at end of file
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
new file mode 100644
index 0000000..da0c55c
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
@@ -0,0 +1,136 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.pseudonym
+
+import dev.dnpm.etl.processor.config.GPasConfigProperties
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpMethod
+import org.springframework.http.HttpStatus
+import org.springframework.retry.policy.SimpleRetryPolicy
+import org.springframework.retry.support.RetryTemplateBuilder
+import org.springframework.test.web.client.MockRestServiceServer
+import org.springframework.test.web.client.match.MockRestRequestMatchers.method
+import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
+import org.springframework.test.web.client.response.MockRestResponseCreators.withException
+import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
+import org.springframework.web.client.RestTemplate
+import java.io.IOException
+
+class GpasPseudonymGeneratorTest {
+
+ private lateinit var mockRestServiceServer: MockRestServiceServer
+ private lateinit var generator: GpasPseudonymGenerator
+ private lateinit var restTemplate: RestTemplate
+
+ @BeforeEach
+ fun setup() {
+ val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
+ val gPasConfigProperties = GPasConfigProperties(
+ "http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
+ "test",
+ null,
+ null,
+ null
+ )
+
+ this.restTemplate = RestTemplate()
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
+ this.generator = GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate)
+ }
+
+ @Test
+ fun shouldReturnExpectedPseudonym() {
+ this.mockRestServiceServer.expect {
+ method(HttpMethod.POST)
+ requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ }.andRespond {
+ withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890"))
+ .createResponse(it)
+ }
+
+ assertThat(this.generator.generate("ID1234")).isEqualTo("test1234ABCDEF567890")
+ }
+
+ @Test
+ fun shouldThrowExceptionIfGpasNotAvailable() {
+ this.mockRestServiceServer.expect {
+ method(HttpMethod.POST)
+ requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ }.andRespond {
+ withException(IOException("Simulated IO error")).createResponse(it)
+ }
+
+ assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
+ }
+
+ @Test
+ fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
+ this.mockRestServiceServer.expect {
+ method(HttpMethod.POST)
+ requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ }.andRespond {
+ withStatus(HttpStatus.FOUND)
+ .header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ .createResponse(it)
+ }
+
+ assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
+ }
+
+ companion object {
+
+ fun getDummyResponseBody(original: String, target: String, pseudonym: String) = """{
+ "resourceType": "Parameters",
+ "parameter": [
+ {
+ "name": "pseudonym",
+ "part": [
+ {
+ "name": "original",
+ "valueIdentifier": {
+ "system": "https://ths-greifswald.de/gpas",
+ "value": "$original"
+ }
+ },
+ {
+ "name": "target",
+ "valueIdentifier": {
+ "system": "https://ths-greifswald.de/gpas",
+ "value": "$target"
+ }
+ },
+ {
+ "name": "pseudonym",
+ "valueIdentifier": {
+ "system": "https://ths-greifswald.de/gpas",
+ "value": "$pseudonym"
+ }
+ }
+ ]
+ }
+ ]
+ }""".trimIndent()
+
+ }
+} \ No newline at end of file
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt
index 88a3a08..47ac301 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt
@@ -19,7 +19,7 @@
package dev.dnpm.etl.processor.services
-import dev.dnpm.etl.processor.AbstractTestcontainerTest
+import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
@@ -37,7 +37,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.transaction.annotation.Transactional
import org.testcontainers.junit.jupiter.Testcontainers
import java.time.Instant
-import java.util.*
@Testcontainers
@ExtendWith(SpringExtension::class)
@@ -66,7 +65,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
@Test
fun shouldResultInEmptyRequestList() {
- val actual = requestService.allRequestsByPatientPseudonym("TEST_12345678901")
+ val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM)
assertThat(actual).isEmpty()
}
@@ -76,33 +75,33 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
this.requestRepository.saveAll(
listOf(
Request(
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.MTB_FILE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-07-07T02:00:00Z")
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-07-07T02:00:00Z")
),
// Should be ignored - wrong patient ID -->
Request(
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678902",
- pid = "P2",
- fingerprint = "0123456789abcdef2",
- type = RequestType.MTB_FILE,
- status = RequestStatus.WARNING,
- processedAt = Instant.parse("2023-08-08T00:00:00Z")
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678902"),
+ PatientId("P2"),
+ Fingerprint("0123456789abcdef2"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-08-08T00:00:00Z")
),
// <--
Request(
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P2",
- fingerprint = "0123456789abcdee1",
- type = RequestType.DELETE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-08-08T02:00:00Z")
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P2"),
+ Fingerprint("0123456789abcdee1"),
+ RequestType.DELETE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-08-08T02:00:00Z")
)
)
)
@@ -112,18 +111,18 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
fun shouldResultInSortedRequestList() {
setupTestData()
- val actual = requestService.allRequestsByPatientPseudonym("TEST_12345678901")
+ val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM)
assertThat(actual).hasSize(2)
- assertThat(actual[0].fingerprint).isEqualTo("0123456789abcdee1")
- assertThat(actual[1].fingerprint).isEqualTo("0123456789abcdef1")
+ assertThat(actual[0].fingerprint).isEqualTo(Fingerprint("0123456789abcdee1"))
+ assertThat(actual[1].fingerprint).isEqualTo(Fingerprint("0123456789abcdef1"))
}
@Test
fun shouldReturnDeleteRequestAsLastRequest() {
setupTestData()
- val actual = requestService.isLastRequestWithKnownStatusDeletion("TEST_12345678901")
+ val actual = requestService.isLastRequestWithKnownStatusDeletion(TEST_PATIENT_PSEUDONYM)
assertThat(actual).isTrue()
}
@@ -132,10 +131,14 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
fun shouldReturnLastMtbFileRequest() {
setupTestData()
- val actual = requestService.lastMtbFileRequestForPatientPseudonym("TEST_12345678901")
+ val actual = requestService.lastMtbFileRequestForPatientPseudonym(TEST_PATIENT_PSEUDONYM)
assertThat(actual).isNotNull
- assertThat(actual?.fingerprint).isEqualTo("0123456789abcdef1")
+ assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef1"))
+ }
+
+ companion object {
+ val TEST_PATIENT_PSEUDONYM = PatientPseudonym("TEST_12345678901")
}
} \ No newline at end of file
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt
index 7fc0121..af4650d 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt
@@ -19,32 +19,51 @@
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.AppSecurityConfiguration
-import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
+import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
+import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
import dev.dnpm.etl.processor.output.MtbFileSender
import dev.dnpm.etl.processor.pseudonym.Generator
+import dev.dnpm.etl.processor.security.Role
import dev.dnpm.etl.processor.services.RequestProcessor
-import dev.dnpm.etl.processor.services.TokenRepository
+import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.TransformationService
+import dev.dnpm.etl.processor.security.UserRoleService
+import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
+import org.springframework.http.MediaType.TEXT_EVENT_STREAM
+import org.springframework.security.test.context.support.WithMockUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestPropertySource
import org.springframework.test.context.junit.jupiter.SpringExtension
-import org.springframework.test.web.servlet.MockMvc
-import org.springframework.test.web.servlet.get
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.*
+import org.springframework.test.web.servlet.client.MockMvcWebTestClient
+import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
+import org.springframework.web.context.WebApplicationContext
import reactor.core.publisher.Sinks
+import reactor.test.StepVerifier
+import java.time.Instant
abstract class MockSink : Sinks.Many<Boolean>
@@ -59,44 +78,48 @@ abstract class MockSink : Sinks.Many<Boolean>
)
@TestPropertySource(
properties = [
- "app.pseudonymize.generator=BUILDIN",
- "app.security.admin-user=admin",
- "app.security.admin-password={noop}very-secret",
- "app.security.enable-tokens=true"
+ "app.pseudonymize.generator=BUILDIN"
]
)
@MockBean(name = "configsUpdateProducer", classes = [MockSink::class])
@MockBean(
Generator::class,
MtbFileSender::class,
- ConnectionCheckService::class,
RequestProcessor::class,
TransformationService::class,
- TokenRepository::class,
- RestConnectionCheckService::class
+ GPasConnectionCheckService::class,
+ RestConnectionCheckService::class,
)
class ConfigControllerTest {
private lateinit var mockMvc: MockMvc
+ private lateinit var webClient: WebClient
private lateinit var requestProcessor: RequestProcessor
+ private lateinit var connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
@BeforeEach
fun setup(
@Autowired mockMvc: MockMvc,
- @Autowired requestProcessor: RequestProcessor
+ @Autowired requestProcessor: RequestProcessor,
+ @Autowired connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) {
this.mockMvc = mockMvc
+ this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
this.requestProcessor = requestProcessor
+ this.connectionCheckUpdateProducer = connectionCheckUpdateProducer
+
+ webClient.options.isThrowExceptionOnScriptError = false
}
@Test
- fun testShouldShowConfigPageIfLoggedIn() {
+ fun testShouldRequestConfigPageIfLoggedIn() {
mockMvc.get("/configs") {
with(user("admin").roles("ADMIN"))
accept(MediaType.TEXT_HTML)
}.andExpect {
status { isOk() }
+ view { name("configs") }
}
}
@@ -113,4 +136,228 @@ class ConfigControllerTest {
}
}
+ @Nested
+ @TestPropertySource(
+ properties = [
+ "app.security.enable-tokens=true",
+ "app.security.admin-user=admin"
+ ]
+ )
+ @MockBean(
+ TokenService::class
+ )
+ inner class WithTokensEnabled {
+ private lateinit var tokenService: TokenService
+
+ @BeforeEach
+ fun setup(
+ @Autowired tokenService: TokenService
+ ) {
+ webClient.options.isThrowExceptionOnScriptError = false
+
+ this.tokenService = tokenService
+ }
+
+ @Test
+ fun testShouldSaveNewToken() {
+ mockMvc.post("/configs/tokens") {
+ with(user("admin").roles("ADMIN"))
+ accept(MediaType.TEXT_HTML)
+ contentType = MediaType.APPLICATION_FORM_URLENCODED
+ content = "name=Testtoken"
+ }.andExpect {
+ status { is2xxSuccessful() }
+ view { name("configs/tokens") }
+ }
+
+ val captor = argumentCaptor<String>()
+ verify(tokenService, times(1)).addToken(captor.capture())
+
+ assertThat(captor.firstValue).isEqualTo("Testtoken")
+ }
+
+ @Test
+ fun testShouldNotSaveTokenWithExstingName() {
+ whenever(tokenService.addToken(anyString())).thenReturn(Result.failure(RuntimeException("Testfailure")))
+
+ mockMvc.post("/configs/tokens") {
+ with(user("admin").roles("ADMIN"))
+ accept(MediaType.TEXT_HTML)
+ contentType = MediaType.APPLICATION_FORM_URLENCODED
+ content = "name=Testtoken"
+ }.andExpect {
+ status { is2xxSuccessful() }
+ view { name("configs/tokens") }
+ }
+
+ val captor = argumentCaptor<String>()
+ verify(tokenService, times(1)).addToken(captor.capture())
+
+ assertThat(captor.firstValue).isEqualTo("Testtoken")
+ }
+
+ @Test
+ fun testShouldDeleteToken() {
+ mockMvc.delete("/configs/tokens/42") {
+ with(user("admin").roles("ADMIN"))
+ accept(MediaType.TEXT_HTML)
+ }.andExpect {
+ status { is2xxSuccessful() }
+ view { name("configs/tokens") }
+ }
+
+ val captor = argumentCaptor<Long>()
+ verify(tokenService, times(1)).deleteToken(captor.capture())
+
+ assertThat(captor.firstValue).isEqualTo(42)
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldRenderConfigPageWithTokens() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/configs")
+ assertThat(
+ page.getElementById("tokens")
+ ).isNotNull
+ }
+ }
+
+ @Nested
+ @TestPropertySource(
+ properties = [
+ "app.security.enable-tokens=false"
+ ]
+ )
+ inner class WithTokensDisabled {
+ @BeforeEach
+ fun setup() {
+ webClient.options.isThrowExceptionOnScriptError = false
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldRenderConfigPageWithoutTokens() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/configs")
+ assertThat(
+ page.getElementById("tokens")
+ ).isNull()
+ }
+ }
+
+ @Nested
+ @TestPropertySource(
+ properties = [
+ "app.security.enable-tokens=false",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret"
+ ]
+ )
+ @MockBean(
+ UserRoleService::class
+ )
+ inner class WithUserRolesEnabled {
+ private lateinit var userRoleService: UserRoleService
+
+ @BeforeEach
+ fun setup(
+ @Autowired userRoleService: UserRoleService
+ ) {
+ webClient.options.isThrowExceptionOnScriptError = false
+
+ this.userRoleService = userRoleService
+ }
+
+ @Test
+ fun testShouldDeleteUserRole() {
+ mockMvc.delete("/configs/userroles/42") {
+ with(user("admin").roles("ADMIN"))
+ accept(MediaType.TEXT_HTML)
+ }.andExpect {
+ status { is2xxSuccessful() }
+ view { name("configs/userroles") }
+ }
+
+ val captor = argumentCaptor<Long>()
+ verify(userRoleService, times(1)).deleteUserRole(captor.capture())
+
+ assertThat(captor.firstValue).isEqualTo(42)
+ }
+
+ @Test
+ fun testShouldUpdateUserRole() {
+ mockMvc.put("/configs/userroles/42") {
+ with(user("admin").roles("ADMIN"))
+ accept(MediaType.TEXT_HTML)
+ contentType = MediaType.APPLICATION_FORM_URLENCODED
+ content = "role=ADMIN"
+ }.andExpect {
+ status { is2xxSuccessful() }
+ view { name("configs/userroles") }
+ }
+
+ val idCaptor = argumentCaptor<Long>()
+ val roleCaptor = argumentCaptor<Role>()
+ verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture())
+
+ assertThat(idCaptor.firstValue).isEqualTo(42)
+ assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN)
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldRenderConfigPageWithUserRoles() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/configs")
+ assertThat(
+ page.getElementById("userroles")
+ ).isNotNull
+ }
+ }
+
+ @Nested
+ inner class WithUserRolesDisabled {
+ @BeforeEach
+ fun setup() {
+ webClient.options.isThrowExceptionOnScriptError = false
+ }
+
+ @Test
+ fun testShouldRenderConfigPageWithoutUserRoles() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/configs")
+ assertThat(
+ page.getElementById("userroles")
+ ).isNull()
+ }
+ }
+
+ @Nested
+ inner class SseTest {
+ private lateinit var webClient: WebTestClient
+
+ @BeforeEach
+ fun setup(
+ applicationContext: WebApplicationContext,
+ ) {
+ this.webClient = MockMvcWebTestClient
+ .bindToApplicationContext(applicationContext).build()
+ }
+
+ @Test
+ fun testShouldRequestSSE() {
+ val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now())
+
+ connectionCheckUpdateProducer.tryEmitNext(expectedEvent)
+ connectionCheckUpdateProducer.emitComplete { _, _ -> true }
+
+ val result = webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM).exchange()
+ .expectStatus().isOk()
+ .expectHeader().contentType(TEXT_EVENT_STREAM)
+ .returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java)
+
+ StepVerifier.create(result.responseBody)
+ .expectNext(expectedEvent)
+ .expectComplete()
+ .verify()
+ }
+ }
+
}
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt
new file mode 100644
index 0000000..82835b4
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt
@@ -0,0 +1,287 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import com.gargoylesoftware.htmlunit.WebClient
+import com.gargoylesoftware.htmlunit.html.HtmlPage
+import dev.dnpm.etl.processor.*
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.monitoring.Report
+import dev.dnpm.etl.processor.monitoring.Request
+import dev.dnpm.etl.processor.monitoring.RequestStatus
+import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.services.RequestService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.PageImpl
+import org.springframework.data.domain.Pageable
+import org.springframework.security.test.context.support.WithMockUser
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
+import java.io.IOException
+import java.time.Instant
+import java.util.*
+
+@WebMvcTest(controllers = [HomeController::class])
+@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
+@ContextConfiguration(
+ classes = [
+ HomeController::class,
+ AppConfiguration::class,
+ AppSecurityConfiguration::class
+ ]
+)
+@TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=BUILDIN",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret"
+ ]
+)
+@MockBean(
+ RequestService::class
+)
+class HomeControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+ private lateinit var webClient: WebClient
+
+ @BeforeEach
+ fun setup(
+ @Autowired mockMvc: MockMvc,
+ @Autowired requestService: RequestService
+ ) {
+ this.mockMvc = mockMvc
+ this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
+
+ whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty())
+ }
+
+ @Test
+ fun testShouldRequestHomePage() {
+ mockMvc.get("/").andExpect {
+ status { isOk() }
+ view { name("index") }
+ }
+ }
+
+ @Nested
+ inner class WithRequests {
+
+ private lateinit var requestService: RequestService
+
+ @BeforeEach
+ fun setup(
+ @Autowired requestService: RequestService
+ ) {
+ this.requestService = requestService
+ }
+
+ @Test
+ fun testShouldShowHomePage() {
+ whenever(requestService.findAll(any<Pageable>())).thenReturn(
+ PageImpl(
+ listOf(
+ Request(
+ 2L,
+ randomRequestId(),
+ PatientPseudonym("PSEUDO1"),
+ PatientId("PATIENT1"),
+ Fingerprint("ashdkasdh"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS
+ ),
+ Request(
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("PSEUDO1"),
+ PatientId("PATIENT1"),
+ Fingerprint("asdasdasd"),
+ RequestType.MTB_FILE,
+ RequestStatus.ERROR
+ )
+ )
+ )
+ )
+
+ val page = webClient.getPage<HtmlPage>("http://localhost/")
+ assertThat(page.querySelectorAll("tbody tr")).hasSize(2)
+ assertThat(page.querySelectorAll("div.notification.info")).isEmpty()
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldShowRequestDetails() {
+ val requestId = randomRequestId()
+
+ whenever(requestService.findByUuid(anyValueClass())).thenReturn(
+ Optional.of(
+ Request(
+ 2L,
+ requestId,
+ PatientPseudonym("PSEUDO1"),
+ PatientId("PATIENT1"),
+ Fingerprint("ashdkasdh"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.now(),
+ Report("Test")
+ )
+ )
+ )
+
+ val page = webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}")
+ assertThat(page.querySelectorAll("tbody tr")).hasSize(1)
+ assertThat(page.querySelectorAll("div.notification.info")).isEmpty()
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldShowPatientDetails() {
+ whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn(
+ PageImpl(
+ listOf(
+ Request(
+ 2L,
+ randomRequestId(),
+ PatientPseudonym("PSEUDO1"),
+ PatientId("PATIENT1"),
+ Fingerprint("ashdkasdh"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS
+ ),
+ Request(
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("PSEUDO1"),
+ PatientId("PATIENT1"),
+ Fingerprint("asdasdasd"),
+ RequestType.MTB_FILE,
+ RequestStatus.ERROR
+ )
+ )
+ )
+ )
+
+ val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1")
+ assertThat(page.querySelectorAll("tbody tr")).hasSize(2)
+ assertThat(page.querySelectorAll("div.notification.info")).isEmpty()
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldShowPatientPseudonym() {
+ whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn(
+ PageImpl(
+ listOf(
+ Request(
+ 2L,
+ randomRequestId(),
+ PatientPseudonym("PSEUDO1"),
+ PatientId("PATIENT1"),
+ Fingerprint("ashdkasdh"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS
+ ),
+ Request(
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("PSEUDO1"),
+ PatientId("PATIENT1"),
+ Fingerprint("asdasdasd"),
+ RequestType.MTB_FILE,
+ RequestStatus.ERROR
+ )
+ )
+ )
+ )
+
+ val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1")
+ assertThat(page.querySelectorAll("h2 > span")).hasSize(1)
+ assertThat(page.querySelectorAll("h2 > span").first().textContent).isEqualTo("PSEUDO1")
+ }
+
+ }
+
+ @Nested
+ inner class WithoutRequests {
+
+ private lateinit var requestService: RequestService
+
+ @BeforeEach
+ fun setup(
+ @Autowired requestService: RequestService
+ ) {
+ this.requestService = requestService
+
+ whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty())
+ }
+
+ @Test
+ fun testShouldShowHomePage() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/")
+ assertThat(page.querySelectorAll("tbody tr")).isEmpty()
+ assertThat(page.querySelectorAll("div.notification.info")).hasSize(1)
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldThrowNotFoundExceptionForUnknownReport() {
+ val requestId = randomRequestId()
+
+ whenever(requestService.findByUuid(anyValueClass())).thenReturn(
+ Optional.empty()
+ )
+
+ assertThrows<IOException> {
+ webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}")
+ }.also {
+ assertThat(it).hasRootCauseInstanceOf(NotFoundException::class.java)
+ }
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldShowEmptyPatientDetails() {
+ whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn(Page.empty())
+
+ val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1")
+ assertThat(page.querySelectorAll("tbody tr")).isEmpty()
+ assertThat(page.querySelectorAll("div.notification.info")).hasSize(1)
+ }
+ }
+
+}
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt
new file mode 100644
index 0000000..0471543
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import com.gargoylesoftware.htmlunit.WebClient
+import com.gargoylesoftware.htmlunit.html.HtmlPage
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.security.TokenService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
+
+@WebMvcTest(controllers = [LoginController::class])
+@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
+@ContextConfiguration(
+ classes = [
+ LoginController::class,
+ AppConfiguration::class,
+ AppSecurityConfiguration::class
+ ]
+)
+@TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=BUILDIN",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret",
+ "app.security.enable-tokens=true"
+ ]
+)
+@MockBean(
+ TokenService::class,
+)
+class LoginControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+ private lateinit var webClient: WebClient
+
+ @BeforeEach
+ fun setup(@Autowired mockMvc: MockMvc) {
+ this.mockMvc = mockMvc
+ this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
+ }
+
+ @Test
+ fun testShouldRequestLoginPage() {
+ mockMvc.get("/login").andExpect {
+ status { isOk() }
+ view { name("login") }
+ }
+ }
+
+ @Test
+ fun testShouldShowLoginForm() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/login")
+ assertThat(
+ page.getElementsByTagName("main").first().firstElementChild.getAttribute("class")
+ ).isEqualTo("login-form")
+ }
+}
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt
new file mode 100644
index 0000000..424a0e3
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt
@@ -0,0 +1,73 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import com.gargoylesoftware.htmlunit.WebClient
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder
+
+@WebMvcTest(controllers = [StatisticsController::class])
+@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
+@ContextConfiguration(
+ classes = [
+ StatisticsController::class,
+ AppConfiguration::class,
+ AppSecurityConfiguration::class
+ ]
+)
+@TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=BUILDIN",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret"
+ ]
+)
+class StatisticsControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+ private lateinit var webClient: WebClient
+
+ @BeforeEach
+ fun setup(@Autowired mockMvc: MockMvc) {
+ this.mockMvc = mockMvc
+ this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
+ }
+
+ @Test
+ fun testShouldRequestLoginPage() {
+ mockMvc.get("/statistics").andExpect {
+ status { isOk() }
+ view { name("statistics") }
+ }
+ }
+
+}
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt
new file mode 100644
index 0000000..b9a1338
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt
@@ -0,0 +1,312 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import dev.dnpm.etl.processor.Fingerprint
+import dev.dnpm.etl.processor.PatientId
+import dev.dnpm.etl.processor.PatientPseudonym
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.monitoring.CountedState
+import dev.dnpm.etl.processor.monitoring.Request
+import dev.dnpm.etl.processor.monitoring.RequestStatus
+import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.randomRequestId
+import dev.dnpm.etl.processor.services.RequestService
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.hasSize
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.whenever
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.http.MediaType.TEXT_EVENT_STREAM
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.client.MockMvcWebTestClient
+import org.springframework.test.web.servlet.get
+import org.springframework.web.context.WebApplicationContext
+import reactor.core.publisher.Sinks
+import reactor.test.StepVerifier
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+
+
+@WebMvcTest(controllers = [StatisticsRestController::class])
+@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
+@ContextConfiguration(
+ classes = [
+ StatisticsRestController::class,
+ AppConfiguration::class,
+ AppSecurityConfiguration::class
+ ]
+)
+@TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=BUILDIN",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret"
+ ]
+)
+@MockBean(
+ RequestService::class
+)
+class StatisticsRestControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+
+ private lateinit var statisticsUpdateProducer: Sinks.Many<Any>
+ private lateinit var requestService: RequestService
+
+ @BeforeEach
+ fun setup(
+ @Autowired mockMvc: MockMvc,
+ @Autowired statisticsUpdateProducer: Sinks.Many<Any>,
+ @Autowired requestService: RequestService
+ ) {
+ this.mockMvc = mockMvc
+ this.statisticsUpdateProducer = statisticsUpdateProducer
+ this.requestService = requestService
+ }
+
+ @Nested
+ inner class RequestStatesTest {
+ @Test
+ fun testShouldRequestStatesForMtbFiles() {
+ doAnswer { _ ->
+ listOf(
+ CountedState(42, RequestStatus.WARNING),
+ CountedState(1, RequestStatus.UNKNOWN)
+ )
+ }.whenever(requestService).countStates()
+
+ mockMvc.get("/statistics/requeststates").andExpect {
+ status { isOk() }.also {
+ jsonPath("$", hasSize<Int>(2))
+ jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name))
+ jsonPath("$[0].value", equalTo(42))
+ jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name))
+ jsonPath("$[1].value", equalTo(1))
+ }
+ }
+ }
+
+ @Test
+ fun testShouldRequestStatesForDeletes() {
+ doAnswer { _ ->
+ listOf(
+ CountedState(42, RequestStatus.SUCCESS),
+ CountedState(1, RequestStatus.ERROR)
+ )
+ }.whenever(requestService).countDeleteStates()
+
+ mockMvc.get("/statistics/requeststates?delete=true").andExpect {
+ status { isOk() }.also {
+ jsonPath("$", hasSize<Int>(2))
+ jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name))
+ jsonPath("$[0].value", equalTo(42))
+ jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name))
+ jsonPath("$[1].value", equalTo(1))
+ }
+ }
+ }
+ }
+
+ @Nested
+ inner class PatientRequestStatesTest {
+ @Test
+ fun testShouldRequestPatientStatesForMtbFiles() {
+ doAnswer { _ ->
+ listOf(
+ CountedState(42, RequestStatus.WARNING),
+ CountedState(1, RequestStatus.UNKNOWN)
+ )
+ }.whenever(requestService).findPatientUniqueStates()
+
+ mockMvc.get("/statistics/requestpatientstates").andExpect {
+ status { isOk() }.also {
+ jsonPath("$", hasSize<Int>(2))
+ jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name))
+ jsonPath("$[0].value", equalTo(42))
+ jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name))
+ jsonPath("$[1].value", equalTo(1))
+ }
+ }
+ }
+
+ @Test
+ fun testShouldRequestPatientStatesForDeletes() {
+ doAnswer { _ ->
+ listOf(
+ CountedState(42, RequestStatus.SUCCESS),
+ CountedState(1, RequestStatus.ERROR)
+ )
+ }.whenever(requestService).findPatientUniqueDeleteStates()
+
+ mockMvc.get("/statistics/requestpatientstates?delete=true").andExpect {
+ status { isOk() }.also {
+ jsonPath("$", hasSize<Int>(2))
+ jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name))
+ jsonPath("$[0].value", equalTo(42))
+ jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name))
+ jsonPath("$[1].value", equalTo(1))
+ }
+ }
+ }
+ }
+
+ @Nested
+ inner class LastMonthStatesTest {
+
+ @BeforeEach
+ fun setup() {
+ doAnswer { _ ->
+ listOf(
+ Request(
+ 1,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS)
+ ),
+ Request(
+ 2,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678902"),
+ PatientId("P2"),
+ Fingerprint("0123456789abcdef2"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS)
+ ),
+ Request(
+ 3,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P2"),
+ Fingerprint("0123456789abcdee1"),
+ RequestType.DELETE,
+ RequestStatus.ERROR,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS)
+ ),
+ Request(
+ 4,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678902"),
+ PatientId("P2"),
+ Fingerprint("0123456789abcdef2"),
+ RequestType.MTB_FILE,
+ RequestStatus.DUPLICATION,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS)
+ ),
+ Request(
+ 5,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678902"),
+ PatientId("P2"),
+ Fingerprint("0123456789abcdef2"),
+ RequestType.DELETE,
+ RequestStatus.UNKNOWN,
+ Instant.now().truncatedTo(ChronoUnit.DAYS)
+ ),
+ )
+ }.whenever(requestService).findAll()
+ }
+
+ @Test
+ fun testShouldRequestLastMonthForMtbFiles() {
+ mockMvc.get("/statistics/requestslastmonth").andExpect {
+ status { isOk() }.also {
+ jsonPath("$", hasSize<Int>(31))
+ }.also {
+ jsonPath("$[28].nameValues.error", equalTo(0))
+ jsonPath("$[28].nameValues.warning", equalTo(1))
+ jsonPath("$[28].nameValues.success", equalTo(1))
+ jsonPath("$[28].nameValues.duplication", equalTo(0))
+ jsonPath("$[28].nameValues.unknown", equalTo(0))
+ jsonPath("$[29].nameValues.error", equalTo(0))
+ jsonPath("$[29].nameValues.warning", equalTo(0))
+ jsonPath("$[29].nameValues.success", equalTo(0))
+ jsonPath("$[29].nameValues.duplication", equalTo(1))
+ jsonPath("$[29].nameValues.unknown", equalTo(0))
+ }
+ }
+ }
+
+ @Test
+ fun testShouldRequestLastMonthForDeletes() {
+ mockMvc.get("/statistics/requestslastmonth?delete=true").andExpect {
+ status { isOk() }.also {
+ jsonPath("$", hasSize<Int>(31))
+ }.also {
+ jsonPath("$[29].nameValues.error", equalTo(1))
+ jsonPath("$[29].nameValues.warning", equalTo(0))
+ jsonPath("$[29].nameValues.success", equalTo(0))
+ jsonPath("$[29].nameValues.duplication", equalTo(0))
+ jsonPath("$[29].nameValues.unknown", equalTo(0))
+ jsonPath("$[30].nameValues.error", equalTo(0))
+ jsonPath("$[30].nameValues.warning", equalTo(0))
+ jsonPath("$[30].nameValues.success", equalTo(0))
+ jsonPath("$[30].nameValues.duplication", equalTo(0))
+ jsonPath("$[30].nameValues.unknown", equalTo(1))
+ }
+ }
+ }
+ }
+
+ @Nested
+ inner class SseTest {
+ private lateinit var webClient: WebTestClient
+
+ @BeforeEach
+ fun setup(
+ applicationContext: WebApplicationContext,
+ ) {
+ this.webClient = MockMvcWebTestClient
+ .bindToApplicationContext(applicationContext).build()
+ }
+
+ @Test
+ fun testShouldRequestSSE() {
+ statisticsUpdateProducer.emitComplete { _, _ -> true }
+
+ val result = webClient.get().uri("http://localhost/statistics/events").accept(TEXT_EVENT_STREAM).exchange()
+ .expectStatus().isOk()
+ .expectHeader().contentType(TEXT_EVENT_STREAM)
+ .returnResult(String::class.java)
+
+ StepVerifier.create(result.responseBody)
+ .expectComplete()
+ .verify()
+ }
+ }
+
+}
diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
index 446bd16..77caa77 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
@@ -23,41 +23,17 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import dev.dnpm.etl.processor.config.GPasConfigProperties;
import org.apache.commons.lang3.StringUtils;
-import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
-import org.apache.hc.client5.http.impl.classic.HttpClients;
-import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
-import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
-import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
-import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
-import org.apache.hc.core5.http.config.Registry;
-import org.apache.hc.core5.http.config.RegistryBuilder;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType;
import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
-import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManagerFactory;
-import java.io.BufferedInputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.Base64;
-
public class GpasPseudonymGenerator implements Generator {
private final static FhirContext r4Context = FhirContext.forR4();
@@ -69,27 +45,13 @@ public class GpasPseudonymGenerator implements Generator {
private final RestTemplate restTemplate;
- private SSLContext customSslContext;
-
- public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate) {
+ public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) {
this.retryTemplate = retryTemplate;
- this.restTemplate = getRestTemplete();
-
+ this.restTemplate = restTemplate;
this.gPasUrl = gpasCfg.getUri();
this.psnTargetDomain = gpasCfg.getTarget();
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
- try {
- if (StringUtils.isNotBlank(gpasCfg.getSslCaLocation())) {
- customSslContext = getSslContext(gpasCfg.getSslCaLocation());
- log.warn(String.format("%s has been initialized with SSL certificate %s. This is deprecated in favor of including Root CA.",
- this.getClass().getName(), gpasCfg.getSslCaLocation()));
- }
- } catch (IOException | KeyManagementException | KeyStoreException | CertificateException |
- NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
-
log.debug(String.format("%s has been initialized", this.getClass().getName()));
}
@@ -99,7 +61,7 @@ public class GpasPseudonymGenerator implements Generator {
var gPasRequestBody = getGpasRequestBody(id);
var responseEntity = getGpasPseudonym(gPasRequestBody);
var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
- .parseResource(responseEntity.getBody());
+ .parseResource(responseEntity.getBody());
return unwrapPseudonym(gPasPseudonymResult);
}
@@ -113,9 +75,9 @@ public class GpasPseudonymGenerator implements Generator {
}
final var identifier = (Identifier) parameters.get().getPart().stream()
- .filter(a -> a.getName().equals("pseudonym"))
- .findFirst()
- .orElseGet(ParametersParameterComponent::new).getValue();
+ .filter(a -> a.getName().equals("pseudonym"))
+ .findFirst()
+ .orElseGet(ParametersParameterComponent::new).getValue();
// pseudonym
return sanitizeValue(identifier.getValue());
@@ -144,8 +106,8 @@ public class GpasPseudonymGenerator implements Generator {
try {
responseEntity = retryTemplate.execute(
- ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
- String.class));
+ ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
+ String.class));
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
@@ -157,16 +119,16 @@ public class GpasPseudonymGenerator implements Generator {
return responseEntity;
} catch (Exception unexpected) {
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) {
var requestParameters = new Parameters();
requestParameters.addParameter().setName("target")
- .setValue(new StringType().setValue(psnTargetDomain));
+ .setValue(new StringType().setValue(psnTargetDomain));
requestParameters.addParameter().setName("original")
- .setValue(new StringType().setValue(id));
+ .setValue(new StringType().setValue(id));
final IParser iParser = r4Context.newJsonParser();
return iParser.encodeResourceToString(requestParameters);
}
@@ -180,67 +142,7 @@ public class GpasPseudonymGenerator implements Generator {
return headers;
}
- String authHeader = gPasUserName + ":" + gPasPassword;
- byte[] authHeaderBytes = authHeader.getBytes();
- byte[] encodedAuthHeaderBytes = Base64.getEncoder().encode(authHeaderBytes);
- String encodedAuthHeader = new String(encodedAuthHeaderBytes);
-
- if (StringUtils.isNotBlank(gPasUserName) && StringUtils.isNotBlank(gPasPassword)) {
- headers.set("Authorization", "Basic " + encodedAuthHeader);
- }
-
+ headers.setBasicAuth(gPasUserName, gPasPassword);
return headers;
}
-
- /**
- * Read SSL root certificate and return SSLContext
- *
- * @param certificateLocation file location to root certificate (PEM)
- * @return initialized SSLContext
- * @throws IOException file cannot be read
- * @throws CertificateException in case we have an invalid certificate of type X.509
- * @throws KeyStoreException keystore cannot be initialized
- * @throws NoSuchAlgorithmException missing trust manager algorithmus
- * @throws KeyManagementException key management failed at init SSLContext
- */
- @Nullable
- protected SSLContext getSslContext(String certificateLocation)
- throws IOException, CertificateException, KeyStoreException, KeyManagementException, NoSuchAlgorithmException {
-
- KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
-
- FileInputStream fis = new FileInputStream(certificateLocation);
- X509Certificate ca = (X509Certificate) CertificateFactory.getInstance("X.509")
- .generateCertificate(new BufferedInputStream(fis));
-
- ks.load(null, null);
- ks.setCertificateEntry(Integer.toString(1), ca);
-
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(
- TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(ks);
-
- SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(null, tmf.getTrustManagers(), null);
-
- return sslContext;
- }
-
- protected RestTemplate getRestTemplete() {
- if (customSslContext == null) {
- return new RestTemplate();
- }
- final var sslsf = new SSLConnectionSocketFactory(customSslContext);
- final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
- .register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build();
-
- final BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(
- socketFactoryRegistry);
- final CloseableHttpClient httpClient = HttpClients.custom()
- .setConnectionManager(connectionManager).build();
-
- final HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(
- httpClient);
- return new RestTemplate(requestFactory);
- }
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
index d951c60..dd7e461 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -69,6 +69,8 @@ data class GPasConfigProperties(
@ConfigurationProperties(RestTargetProperties.NAME)
data class RestTargetProperties(
val uri: String?,
+ val username: String?,
+ val password: String?,
) {
companion object {
const val NAME = "app.rest"
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
index 0ae2c2f..5fc1120 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt
@@ -20,21 +20,32 @@
package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper
-import dev.dnpm.etl.processor.monitoring.*
+import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
+import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
+import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
+import dev.dnpm.etl.processor.monitoring.ReportService
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
-import dev.dnpm.etl.processor.services.TokenRepository
-import dev.dnpm.etl.processor.services.TokenService
+import dev.dnpm.etl.processor.security.TokenRepository
+import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.Transformation
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.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
import org.springframework.retry.RetryCallback
import org.springframework.retry.RetryContext
import org.springframework.retry.RetryListener
@@ -46,6 +57,13 @@ import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.web.client.RestTemplate
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.toJavaDuration
@@ -70,8 +88,20 @@ class AppConfiguration {
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS")
@Bean
- fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate): Generator {
- return GpasPseudonymGenerator(configProperties, retryTemplate)
+ 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)
}
@ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true)
@@ -83,8 +113,80 @@ class AppConfiguration {
@ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS")
@ConditionalOnMissingBean
@Bean
- fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate): Generator {
- return GpasPseudonymGenerator(configProperties, retryTemplate)
+ 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")
@@ -173,5 +275,9 @@ class AppConfiguration {
return GPasConnectionCheckService(restTemplate, gPasConfigProperties, connectionCheckUpdateProducer)
}
+ @Bean
+ fun jdbcConfiguration(): AbstractJdbcConfiguration {
+ return AppJdbcConfiguration()
+ }
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt
new file mode 100644
index 0000000..898982c
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt
@@ -0,0 +1,25 @@
+package dev.dnpm.etl.processor.config
+
+import dev.dnpm.etl.processor.Fingerprint
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.convert.converter.Converter
+import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration
+
+@Configuration
+class AppJdbcConfiguration : AbstractJdbcConfiguration() {
+ override fun userConverters(): MutableList<*> {
+ return mutableListOf(StringToFingerprintConverter(), FingerprintToStringConverter())
+ }
+}
+
+class StringToFingerprintConverter : Converter<String, Fingerprint> {
+ override fun convert(source: String): Fingerprint {
+ return Fingerprint(source)
+ }
+}
+
+class FingerprintToStringConverter : Converter<Fingerprint, String> {
+ override fun convert(source: Fingerprint): String {
+ return source.value
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
index c377555..6b063bd 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt
@@ -21,7 +21,7 @@ package dev.dnpm.etl.processor.config
import dev.dnpm.etl.processor.security.UserRole
import dev.dnpm.etl.processor.security.UserRoleRepository
-import dev.dnpm.etl.processor.services.UserRoleService
+import dev.dnpm.etl.processor.security.UserRoleService
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
@@ -89,7 +89,7 @@ class AppSecurityConfiguration(
http {
authorizeRequests {
authorize("/configs/**", hasRole("ADMIN"))
- authorize("/mtbfile/**", hasAnyRole("MTBFILE"))
+ authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER"))
authorize("/report/**", hasAnyRole("ADMIN", "USER"))
authorize("*.css", permitAll)
authorize("*.ico", permitAll)
@@ -147,7 +147,7 @@ class AppSecurityConfiguration(
http {
authorizeRequests {
authorize("/configs/**", hasRole("ADMIN"))
- authorize("/mtbfile/**", hasAnyRole("MTBFILE"))
+ authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN"))
authorize("/report/**", hasRole("ADMIN"))
authorize(anyRequest, permitAll)
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
index de901ce..2aff8cb 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt
@@ -22,6 +22,8 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.PatientId
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.services.RequestProcessor
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.slf4j.LoggerFactory
@@ -35,11 +37,12 @@ class KafkaInputListener(
override fun onMessage(data: ConsumerRecord<String, String>) {
val mtbFile = objectMapper.readValue(data.value(), MtbFile::class.java)
+ val patientId = PatientId(mtbFile.patient.id)
val firstRequestIdHeader = data.headers().headers("requestId")?.firstOrNull()
val requestId = if (null != firstRequestIdHeader) {
- String(firstRequestIdHeader.value())
+ RequestId(String(firstRequestIdHeader.value()))
} else {
- ""
+ RequestId("")
}
if (mtbFile.consent.status == Consent.Status.ACTIVE) {
@@ -52,9 +55,9 @@ class KafkaInputListener(
} else {
logger.debug("Accepted MTB File and process deletion")
if (requestId.isBlank()) {
- requestProcessor.processDeletion(mtbFile.patient.id)
+ requestProcessor.processDeletion(patientId)
} else {
- requestProcessor.processDeletion(mtbFile.patient.id, requestId)
+ requestProcessor.processDeletion(patientId, requestId)
}
}
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt
index 8259288..9e282c2 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt
@@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.input
import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.PatientId
import dev.dnpm.etl.processor.services.RequestProcessor
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
@@ -46,7 +47,8 @@ class MtbFileRestController(
requestProcessor.processMtbFile(mtbFile)
} else {
logger.debug("Accepted MTB File and process deletion")
- requestProcessor.processDeletion(mtbFile.patient.id)
+ val patientId = PatientId(mtbFile.patient.id)
+ requestProcessor.processDeletion(patientId)
}
return ResponseEntity.accepted().build()
}
@@ -54,7 +56,7 @@ class MtbFileRestController(
@DeleteMapping(path = ["{patientId}"])
fun deleteData(@PathVariable patientId: String): ResponseEntity<Void> {
logger.debug("Accepted patient ID to process deletion")
- requestProcessor.processDeletion(patientId)
+ requestProcessor.processDeletion(PatientId(patientId))
return ResponseEntity.accepted().build()
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
index 81ad922..e70da3e 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt
@@ -26,22 +26,18 @@ import jakarta.annotation.PostConstruct
import org.apache.kafka.clients.consumer.Consumer
import org.apache.kafka.common.errors.TimeoutException
import org.springframework.beans.factory.annotation.Qualifier
-import org.springframework.http.HttpEntity
-import org.springframework.http.HttpHeaders
-import org.springframework.http.HttpMethod
-import org.springframework.http.HttpStatus
-import org.springframework.http.MediaType
-import org.springframework.http.RequestEntity
+import org.springframework.http.*
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.web.client.RestTemplate
import org.springframework.web.util.UriComponentsBuilder
import reactor.core.publisher.Sinks
+import java.time.Instant
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
interface ConnectionCheckService {
- fun connectionAvailable(): Boolean
+ fun connectionAvailable(): ConnectionCheckResult
}
@@ -51,9 +47,27 @@ sealed class ConnectionCheckResult {
abstract val available: Boolean
- data class KafkaConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
- data class RestConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
- data class GPasConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
+ abstract val timestamp: Instant
+
+ abstract val lastChange: Instant
+
+ data class KafkaConnectionCheckResult(
+ override val available: Boolean,
+ override val timestamp: Instant,
+ override val lastChange: Instant
+ ) : ConnectionCheckResult()
+
+ data class RestConnectionCheckResult(
+ override val available: Boolean,
+ override val timestamp: Instant,
+ override val lastChange: Instant
+ ) : ConnectionCheckResult()
+
+ data class GPasConnectionCheckResult(
+ override val available: Boolean,
+ override val timestamp: Instant,
+ override val lastChange: Instant
+ ) : ConnectionCheckResult()
}
class KafkaConnectionCheckService(
@@ -62,25 +76,33 @@ class KafkaConnectionCheckService(
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) : OutputConnectionCheckService {
- private var connectionAvailable: Boolean = false
-
+ private var result = ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now())
@PostConstruct
@Scheduled(cron = "0 * * * * *")
fun check() {
- connectionAvailable = try {
- null != consumer.listTopics(5.seconds.toJavaDuration())
+ result = try {
+ val available = null != consumer.listTopics(5.seconds.toJavaDuration())
+ ConnectionCheckResult.KafkaConnectionCheckResult(
+ available,
+ Instant.now(),
+ if (result.available == available) { result.lastChange } else { Instant.now() }
+ )
} catch (e: TimeoutException) {
- false
+ ConnectionCheckResult.KafkaConnectionCheckResult(
+ false,
+ Instant.now(),
+ if (!result.available) { result.lastChange } else { Instant.now() }
+ )
}
connectionCheckUpdateProducer.emitNext(
- ConnectionCheckResult.KafkaConnectionCheckResult(connectionAvailable),
+ result,
Sinks.EmitFailureHandler.FAIL_FAST
)
}
- override fun connectionAvailable(): Boolean {
- return this.connectionAvailable
+ override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult {
+ return this.result
}
}
@@ -92,27 +114,37 @@ class RestConnectionCheckService(
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) : OutputConnectionCheckService {
- private var connectionAvailable: Boolean = false
+ private var result = ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now())
@PostConstruct
@Scheduled(cron = "0 * * * * *")
fun check() {
- connectionAvailable = try {
- restTemplate.getForEntity(
+ result = try {
+ val available = restTemplate.getForEntity(
restTargetProperties.uri?.replace("/etl/api", "").toString(),
String::class.java
).statusCode == HttpStatus.OK
+
+ ConnectionCheckResult.RestConnectionCheckResult(
+ available,
+ Instant.now(),
+ if (result.available == available) { result.lastChange } else { Instant.now() }
+ )
} catch (e: Exception) {
- false
+ ConnectionCheckResult.RestConnectionCheckResult(
+ false,
+ Instant.now(),
+ if (!result.available) { result.lastChange } else { Instant.now() }
+ )
}
connectionCheckUpdateProducer.emitNext(
- ConnectionCheckResult.RestConnectionCheckResult(connectionAvailable),
+ result,
Sinks.EmitFailureHandler.FAIL_FAST
)
}
- override fun connectionAvailable(): Boolean {
- return this.connectionAvailable
+ override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult {
+ return this.result
}
}
@@ -123,40 +155,48 @@ class GPasConnectionCheckService(
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
) : ConnectionCheckService {
- private var connectionAvailable: Boolean = false
+ private var result = ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now())
@PostConstruct
@Scheduled(cron = "0 * * * * *")
fun check() {
- connectionAvailable = try {
+ result = try {
val uri = UriComponentsBuilder.fromUriString(
- gPasConfigProperties.uri?.replace("/\$pseudonymizeAllowCreate", "/\$pseudonymize").toString()
- )
- .queryParam("target", gPasConfigProperties.target)
- .queryParam("original", "???")
- .build().toUri()
+ gPasConfigProperties.uri?.replace("/\$pseudonymizeAllowCreate", "/metadata").toString()
+ ).build().toUri()
val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_JSON
if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) {
headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password)
}
- restTemplate.exchange(
+
+ val available = restTemplate.exchange(
uri,
HttpMethod.GET,
HttpEntity<Void>(headers),
Void::class.java
).statusCode == HttpStatus.OK
+
+ ConnectionCheckResult.GPasConnectionCheckResult(
+ available,
+ Instant.now(),
+ if (result.available == available) { result.lastChange } else { Instant.now() }
+ )
} catch (e: Exception) {
- false
+ ConnectionCheckResult.GPasConnectionCheckResult(
+ false,
+ Instant.now(),
+ if (!result.available) { result.lastChange } else { Instant.now() }
+ )
}
connectionCheckUpdateProducer.emitNext(
- ConnectionCheckResult.GPasConnectionCheckResult(connectionAvailable),
+ result,
Sinks.EmitFailureHandler.FAIL_FAST
)
}
- override fun connectionAvailable(): Boolean {
- return this.connectionAvailable
+ override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
+ return this.result
}
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt
index 97ecd05..062f749 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt
@@ -19,6 +19,7 @@
package dev.dnpm.etl.processor.monitoring
+import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonParseException
@@ -54,7 +55,7 @@ class ReportService(
private data class DataQualityReport(val issues: List<Issue>)
@JsonIgnoreProperties(ignoreUnknown = true)
- data class Issue(val severity: Severity, val message: String)
+ data class Issue(val severity: Severity, @JsonAlias("details") val message: String)
enum class Severity(@JsonValue val value: String) {
FATAL("fatal"),
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
index 028b4a3..36c9705 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt
@@ -19,10 +19,12 @@
package dev.dnpm.etl.processor.monitoring
+import dev.dnpm.etl.processor.*
import org.springframework.data.annotation.Id
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jdbc.repository.query.Query
+import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Embedded
import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
@@ -30,26 +32,48 @@ import org.springframework.data.repository.PagingAndSortingRepository
import java.time.Instant
import java.util.*
-typealias RequestId = UUID
-
@Table("request")
data class Request(
@Id val id: Long? = null,
- val uuid: String = RequestId.randomUUID().toString(),
- val patientId: String,
- val pid: String,
- val fingerprint: String,
+ val uuid: RequestId = randomRequestId(),
+ val patientPseudonym: PatientPseudonym,
+ val pid: PatientId,
+ @Column("fingerprint")
+ val fingerprint: Fingerprint,
val type: RequestType,
var status: RequestStatus,
var processedAt: Instant = Instant.now(),
@Embedded.Nullable var report: Report? = null
-)
+) {
+ constructor(
+ uuid: RequestId,
+ patientPseudonym: PatientPseudonym,
+ pid: PatientId,
+ fingerprint: Fingerprint,
+ type: RequestType,
+ status: RequestStatus
+ ) :
+ this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now())
+
+ constructor(
+ uuid: RequestId,
+ patientPseudonym: PatientPseudonym,
+ pid: PatientId,
+ fingerprint: Fingerprint,
+ type: RequestType,
+ status: RequestStatus,
+ processedAt: Instant
+ ) :
+ this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt)
+}
+@JvmRecord
data class Report(
val description: String,
val dataQualityReport: String = ""
)
+@JvmRecord
data class CountedState(
val count: Int,
val status: RequestStatus,
@@ -57,17 +81,17 @@ data class CountedState(
interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRepository<Request, Long> {
- fun findAllByPatientIdOrderByProcessedAtDesc(patientId: String): List<Request>
+ fun findAllByPatientPseudonymOrderByProcessedAtDesc(patientId: PatientPseudonym): List<Request>
- fun findByUuidEquals(uuid: String): Optional<Request>
+ fun findByUuidEquals(uuid: RequestId): Optional<Request>
- fun findRequestByPatientId(patientId: String, pageable: Pageable): Page<Request>
+ fun findRequestByPatientPseudonym(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request>
@Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;")
fun countStates(): List<CountedState>
@Query("SELECT count(*) AS count, status FROM (" +
- "SELECT status, rank() OVER (PARTITION BY patient_id ORDER BY processed_at DESC) AS rank FROM request " +
+ "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
"WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " +
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
fun findPatientUniqueStates(): List<CountedState>
@@ -76,7 +100,7 @@ interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRep
fun countDeleteStates(): List<CountedState>
@Query("SELECT count(*) AS count, status FROM (" +
- "SELECT status, rank() OVER (PARTITION BY patient_id ORDER BY processed_at DESC) AS rank FROM request " +
+ "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " +
"WHERE type = 'DELETE'" +
") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;")
fun findPatientUniqueDeleteStates(): List<CountedState>
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
index fc5d617..4838689 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt
@@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.output
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.config.KafkaProperties
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.slf4j.LoggerFactory
@@ -62,7 +63,7 @@ class KafkaMtbFileSender(
val dummyMtbFile = MtbFile.builder()
.withConsent(
Consent.builder()
- .withPatient(request.patientId)
+ .withPatient(request.patientId.value)
.withStatus(Consent.Status.REJECTED)
.build()
)
@@ -98,8 +99,8 @@ class KafkaMtbFileSender(
}
private fun key(request: MtbFileSender.DeleteRequest): String {
- return "{\"pid\": \"${request.patientId}\"}"
+ return "{\"pid\": \"${request.patientId.value}\"}"
}
- data class Data(val requestId: String, val content: MtbFile)
+ data class Data(val requestId: RequestId, val content: MtbFile)
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt
index aca972b..8d994c5 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt
@@ -20,6 +20,8 @@
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 org.springframework.http.HttpStatusCode
@@ -32,9 +34,9 @@ interface MtbFileSender {
data class Response(val status: RequestStatus, val body: String = "")
- data class MtbFileRequest(val requestId: String, val mtbFile: MtbFile)
+ data class MtbFileRequest(val requestId: RequestId, val mtbFile: MtbFile)
- data class DeleteRequest(val requestId: String, val patientId: String)
+ data class DeleteRequest(val requestId: RequestId, val patientId: PatientPseudonym)
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
index e1aecb7..58459b9 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt
@@ -40,8 +40,7 @@ class RestMtbFileSender(
override fun send(request: MtbFileSender.MtbFileRequest): MtbFileSender.Response {
try {
return retryTemplate.execute<MtbFileSender.Response, Exception> {
- val headers = HttpHeaders()
- headers.contentType = MediaType.APPLICATION_JSON
+ val headers = getHttpHeaders()
val entityReq = HttpEntity(request.mtbFile, headers)
val response = restTemplate.postForEntity(
"${restTargetProperties.uri}/MTBFile",
@@ -70,8 +69,7 @@ class RestMtbFileSender(
override fun send(request: MtbFileSender.DeleteRequest): MtbFileSender.Response {
try {
return retryTemplate.execute<MtbFileSender.Response, Exception> {
- val headers = HttpHeaders()
- headers.contentType = MediaType.APPLICATION_JSON
+ val headers = getHttpHeaders()
val entityReq = HttpEntity(null, headers)
restTemplate.delete(
"${restTargetProperties.uri}/Patient/${request.patientId}",
@@ -94,4 +92,18 @@ class RestMtbFileSender(
return this.restTargetProperties.uri.orEmpty()
}
+ private fun getHttpHeaders(): HttpHeaders {
+ val username = restTargetProperties.username
+ val password = restTargetProperties.password
+ val headers = HttpHeaders()
+ headers.setContentType(MediaType.APPLICATION_JSON)
+
+ if (username.isNullOrBlank() || password.isNullOrBlank()) {
+ return headers
+ }
+
+ headers.setBasicAuth(username, password)
+ return headers
+ }
+
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
index d18cd2c..e80f6ec 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
@@ -19,6 +19,8 @@
package dev.dnpm.etl.processor.pseudonym
+import dev.dnpm.etl.processor.PatientId
+import dev.dnpm.etl.processor.PatientPseudonym
import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties
class PseudonymizeService(
@@ -26,10 +28,10 @@ class PseudonymizeService(
private val configProperties: PseudonymizeConfigProperties
) {
- fun patientPseudonym(patientId: String): String {
+ fun patientPseudonym(patientId: PatientId): PatientPseudonym {
return when (generator) {
- is GpasPseudonymGenerator -> generator.generate(patientId)
- else -> "${configProperties.prefix}_${generator.generate(patientId)}"
+ is GpasPseudonymGenerator -> PatientPseudonym(generator.generate(patientId.value))
+ else -> PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}")
}
}
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
index ef25787..bf645f6 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
@@ -20,6 +20,7 @@
package dev.dnpm.etl.processor.pseudonym
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.PatientId
import org.apache.commons.codec.digest.DigestUtils
/** Replaces patient ID with generated patient pseudonym
@@ -29,7 +30,7 @@ import org.apache.commons.codec.digest.DigestUtils
* @return The MTB file containing patient pseudonymes
*/
infix fun MtbFile.pseudonymizeWith(pseudonymizeService: PseudonymizeService) {
- val patientPseudonym = pseudonymizeService.patientPseudonym(this.patient.id)
+ val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value
this.episode?.patient = patientPseudonym
this.carePlans?.forEach { it.patient = patientPseudonym }
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt
index f084408..44b04e8 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/TokenService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt
@@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package dev.dnpm.etl.processor.services
+package dev.dnpm.etl.processor.security
import jakarta.annotation.PostConstruct
import org.springframework.data.annotation.Id
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt
index 6649f7d..174f8a9 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/UserRoleService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt
@@ -17,11 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package dev.dnpm.etl.processor.services
+package dev.dnpm.etl.processor.security
-import dev.dnpm.etl.processor.security.Role
-import dev.dnpm.etl.processor.security.UserRole
-import dev.dnpm.etl.processor.security.UserRoleRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.core.session.SessionRegistry
import org.springframework.security.oauth2.core.oidc.user.OidcUser
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
index bdf07cb..f4e6222 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
@@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.services
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.MtbFile
+import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request
@@ -49,25 +50,27 @@ class RequestProcessor(
) {
fun processMtbFile(mtbFile: MtbFile) {
- processMtbFile(mtbFile, UUID.randomUUID().toString())
+ processMtbFile(mtbFile, randomRequestId())
}
- fun processMtbFile(mtbFile: MtbFile, requestId: String) {
- val pid = mtbFile.patient.id
+ fun processMtbFile(mtbFile: MtbFile, requestId: RequestId) {
+ val pid = PatientId(mtbFile.patient.id)
mtbFile pseudonymizeWith pseudonymizeService
mtbFile anonymizeContentWith pseudonymizeService
val request = MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile))
+ val patientPseudonym = PatientPseudonym(request.mtbFile.patient.id)
+
requestService.save(
Request(
- uuid = requestId,
- patientId = request.mtbFile.patient.id,
- pid = pid,
- fingerprint = fingerprint(request.mtbFile),
- status = RequestStatus.UNKNOWN,
- type = RequestType.MTB_FILE
+ requestId,
+ patientPseudonym,
+ pid,
+ fingerprint(request.mtbFile),
+ RequestType.MTB_FILE,
+ RequestStatus.UNKNOWN
)
)
@@ -98,31 +101,33 @@ class RequestProcessor(
}
private fun isDuplication(pseudonymizedMtbFile: MtbFile): Boolean {
+ val patientPseudonym = PatientPseudonym(pseudonymizedMtbFile.patient.id)
+
val lastMtbFileRequestForPatient =
- requestService.lastMtbFileRequestForPatientPseudonym(pseudonymizedMtbFile.patient.id)
- val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(pseudonymizedMtbFile.patient.id)
+ requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym)
+ val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym)
return null != lastMtbFileRequestForPatient
&& !isLastRequestDeletion
&& lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFile)
}
- fun processDeletion(patientId: String) {
- processDeletion(patientId, UUID.randomUUID().toString())
+ fun processDeletion(patientId: PatientId) {
+ processDeletion(patientId, randomRequestId())
}
- fun processDeletion(patientId: String, requestId: String) {
+ fun processDeletion(patientId: PatientId, requestId: RequestId) {
try {
val patientPseudonym = pseudonymizeService.patientPseudonym(patientId)
requestService.save(
Request(
- uuid = requestId,
- patientId = patientPseudonym,
- pid = patientId,
- fingerprint = fingerprint(patientPseudonym),
- status = RequestStatus.UNKNOWN,
- type = RequestType.DELETE
+ requestId,
+ patientPseudonym,
+ patientId,
+ fingerprint(patientPseudonym.value),
+ RequestType.DELETE,
+ RequestStatus.UNKNOWN
)
)
@@ -144,9 +149,9 @@ class RequestProcessor(
requestService.save(
Request(
uuid = requestId,
- patientId = "???",
+ patientPseudonym = emptyPatientPseudonym(),
pid = patientId,
- fingerprint = "",
+ fingerprint = Fingerprint.empty(),
status = RequestStatus.ERROR,
type = RequestType.DELETE,
report = Report("Fehler bei der Pseudonymisierung")
@@ -155,14 +160,16 @@ class RequestProcessor(
}
}
- private fun fingerprint(mtbFile: MtbFile): String {
+ private fun fingerprint(mtbFile: MtbFile): Fingerprint {
return fingerprint(objectMapper.writeValueAsString(mtbFile))
}
- private fun fingerprint(s: String): String {
- return Base32().encodeAsString(DigestUtils.sha256(s))
- .replace("=", "")
- .lowercase()
+ private fun fingerprint(s: String): Fingerprint {
+ return Fingerprint(
+ Base32().encodeAsString(DigestUtils.sha256(s))
+ .replace("=", "")
+ .lowercase()
+ )
}
} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt
index e0043d2..757b353 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt
@@ -19,11 +19,13 @@
package dev.dnpm.etl.processor.services
-import dev.dnpm.etl.processor.monitoring.Request
-import dev.dnpm.etl.processor.monitoring.RequestRepository
-import dev.dnpm.etl.processor.monitoring.RequestStatus
-import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.PatientPseudonym
+import dev.dnpm.etl.processor.RequestId
+import dev.dnpm.etl.processor.monitoring.*
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
+import java.util.*
@Service
class RequestService(
@@ -32,15 +34,32 @@ class RequestService(
fun save(request: Request) = requestRepository.save(request)
- fun allRequestsByPatientPseudonym(patientPseudonym: String) = requestRepository
- .findAllByPatientIdOrderByProcessedAtDesc(patientPseudonym)
+ fun findAll(): Iterable<Request> = requestRepository.findAll()
- fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: String) =
+ fun findAll(pageable: Pageable): Page<Request> = requestRepository.findAll(pageable)
+
+ fun findByUuid(uuid: RequestId): Optional<Request> =
+ requestRepository.findByUuidEquals(uuid)
+
+ fun findRequestByPatientId(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request> = requestRepository.findRequestByPatientPseudonym(patientPseudonym, pageable)
+
+ fun allRequestsByPatientPseudonym(patientPseudonym: PatientPseudonym) = requestRepository
+ .findAllByPatientPseudonymOrderByProcessedAtDesc(patientPseudonym)
+
+ fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: PatientPseudonym) =
Companion.lastMtbFileRequestForPatientPseudonym(allRequestsByPatientPseudonym(patientPseudonym))
- fun isLastRequestWithKnownStatusDeletion(patientPseudonym: String) =
+ fun isLastRequestWithKnownStatusDeletion(patientPseudonym: PatientPseudonym) =
Companion.isLastRequestWithKnownStatusDeletion(allRequestsByPatientPseudonym(patientPseudonym))
+ fun countStates(): Iterable<CountedState> = requestRepository.countStates()
+
+ fun countDeleteStates(): Iterable<CountedState> = requestRepository.countDeleteStates()
+
+ fun findPatientUniqueStates(): List<CountedState> = requestRepository.findPatientUniqueStates()
+
+ fun findPatientUniqueDeleteStates(): List<CountedState> = requestRepository.findPatientUniqueDeleteStates()
+
companion object {
fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = allRequests
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
index 4048348..ecb2ec7 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt
@@ -19,8 +19,8 @@
package dev.dnpm.etl.processor.services
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.monitoring.Report
-import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.slf4j.LoggerFactory
import org.springframework.context.event.EventListener
@@ -31,7 +31,7 @@ import java.util.*
@Service
class ResponseProcessor(
- private val requestRepository: RequestRepository,
+ private val requestService: RequestService,
private val statisticsUpdateProducer: Sinks.Many<Any>
) {
@@ -39,7 +39,7 @@ class ResponseProcessor(
@EventListener(classes = [ResponseEvent::class])
fun handleResponseEvent(event: ResponseEvent) {
- requestRepository.findByUuidEquals(event.requestUuid).ifPresentOrElse({
+ requestService.findByUuid(event.requestUuid).ifPresentOrElse({
it.processedAt = event.timestamp
it.status = event.status
@@ -76,7 +76,7 @@ class ResponseProcessor(
}
}
- requestRepository.save(it)
+ requestService.save(it)
statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST)
}, {
@@ -87,7 +87,7 @@ class ResponseProcessor(
}
data class ResponseEvent(
- val requestUuid: String,
+ val requestUuid: RequestId,
val timestamp: Instant,
val status: RequestStatus,
val body: Optional<String> = Optional.empty()
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt
index a29010f..12e824d 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt
@@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.services.kafka
import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.output.asRequestStatus
import dev.dnpm.etl.processor.services.ResponseEvent
@@ -47,7 +48,7 @@ class KafkaResponseProcessor(
Optional.empty()
}.ifPresentOrElse({ responseBody ->
val event = ResponseEvent(
- responseBody.requestId,
+ RequestId(responseBody.requestId),
Instant.ofEpochMilli(data.timestamp()),
responseBody.statusCode.asRequestStatus(),
when (responseBody.statusCode.asRequestStatus()) {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/types.kt b/src/main/kotlin/dev/dnpm/etl/processor/types.kt
new file mode 100644
index 0000000..b2f13ef
--- /dev/null
+++ b/src/main/kotlin/dev/dnpm/etl/processor/types.kt
@@ -0,0 +1,49 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor
+
+import java.util.*
+
+class Fingerprint(val value: String) {
+ override fun hashCode() = value.hashCode()
+
+ override fun equals(other: Any?) = other is Fingerprint && other.value == value
+
+ companion object {
+ fun empty() = Fingerprint("")
+ }
+}
+
+@JvmInline
+value class RequestId(val value: String) {
+
+ fun isBlank() = value.isBlank()
+
+}
+
+fun randomRequestId() = RequestId(UUID.randomUUID().toString())
+
+@JvmInline
+value class PatientId(val value: String)
+
+@JvmInline
+value class PatientPseudonym(val value: String)
+
+fun emptyPatientPseudonym() = PatientPseudonym("") \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
index eb9d541..25ec7cc 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt
@@ -27,10 +27,10 @@ import dev.dnpm.etl.processor.output.MtbFileSender
import dev.dnpm.etl.processor.pseudonym.Generator
import dev.dnpm.etl.processor.security.Role
import dev.dnpm.etl.processor.security.UserRole
-import dev.dnpm.etl.processor.services.Token
-import dev.dnpm.etl.processor.services.TokenService
+import dev.dnpm.etl.processor.security.Token
+import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.TransformationService
-import dev.dnpm.etl.processor.services.UserRoleService
+import dev.dnpm.etl.processor.security.UserRoleService
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.MediaType
import org.springframework.http.codec.ServerSentEvent
@@ -56,7 +56,7 @@ class ConfigController(
@GetMapping
fun index(model: Model): String {
val outputConnectionAvailable =
- connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().first().connectionAvailable()
+ connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().firstOrNull()?.connectionAvailable()
val gPasConnectionAvailable =
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
@@ -127,10 +127,11 @@ class ConfigController(
} else {
model.addAttribute("tokensEnabled", true)
val result = tokenService.addToken(name)
- if (result.isSuccess) {
- model.addAttribute("newTokenValue", result.getOrDefault(""))
+ result.onSuccess {
+ model.addAttribute("newTokenValue", it)
model.addAttribute("success", true)
- } else {
+ }
+ result.onFailure {
model.addAttribute("success", false)
}
model.addAttribute("tokens", tokenService.findAll())
@@ -182,6 +183,7 @@ class ConfigController(
}
@GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
+ @ResponseBody
fun events(): Flux<ServerSentEvent<Any>> {
return connectionCheckUpdateProducer.asFlux().map {
val event = when (it) {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
index 6a256aa..54920b1 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
@@ -20,9 +20,10 @@
package dev.dnpm.etl.processor.web
import dev.dnpm.etl.processor.NotFoundException
+import dev.dnpm.etl.processor.PatientPseudonym
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.monitoring.ReportService
-import dev.dnpm.etl.processor.monitoring.RequestId
-import dev.dnpm.etl.processor.monitoring.RequestRepository
+import dev.dnpm.etl.processor.services.RequestService
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
@@ -35,7 +36,7 @@ import org.springframework.web.bind.annotation.RequestMapping
@Controller
@RequestMapping(path = ["/"])
class HomeController(
- private val requestRepository: RequestRepository,
+ private val requestService: RequestService,
private val reportService: ReportService
) {
@@ -44,20 +45,20 @@ class HomeController(
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
model: Model
): String {
- val requests = requestRepository.findAll(pageable)
+ val requests = requestService.findAll(pageable)
model.addAttribute("requests", requests)
return "index"
}
- @GetMapping(path = ["patient/{patientId}"])
+ @GetMapping(path = ["patient/{patientPseudonym}"])
fun byPatient(
- @PathVariable patientId: String,
+ @PathVariable patientPseudonym: PatientPseudonym,
@PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable,
model: Model
): String {
- val requests = requestRepository.findRequestByPatientId(patientId, pageable)
- model.addAttribute("patientId", patientId)
+ val requests = requestService.findRequestByPatientId(patientPseudonym, pageable)
+ model.addAttribute("patientPseudonym", patientPseudonym.value)
model.addAttribute("requests", requests)
return "index"
@@ -65,7 +66,7 @@ class HomeController(
@GetMapping(path = ["/report/{id}"])
fun report(@PathVariable id: RequestId, model: Model): String {
- val request = requestRepository.findByUuidEquals(id.toString()).orElse(null) ?: throw NotFoundException()
+ val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException()
model.addAttribute("request", request)
model.addAttribute("issues", reportService.deserialize(request.report?.dataQualityReport))
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt
index daa6af3..c034cb4 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt
@@ -19,9 +19,9 @@
package dev.dnpm.etl.processor.web
-import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
+import dev.dnpm.etl.processor.services.RequestService
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.MediaType
import org.springframework.http.codec.ServerSentEvent
@@ -41,15 +41,15 @@ import java.time.temporal.ChronoUnit
class StatisticsRestController(
@Qualifier("statisticsUpdateProducer")
private val statisticsUpdateProducer: Sinks.Many<Any>,
- private val requestRepository: RequestRepository
+ private val requestService: RequestService
) {
@GetMapping(path = ["requeststates"])
fun requestStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
val states = if (delete) {
- requestRepository.countDeleteStates()
+ requestService.countDeleteStates()
} else {
- requestRepository.countStates()
+ requestService.countStates()
}
return states
@@ -79,7 +79,7 @@ class StatisticsRestController(
}
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Europe/Berlin"))
- val data = requestRepository.findAll()
+ val data = requestService.findAll()
.filter { it.type == requestType }
.filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) }
.groupBy { formatter.format(it.processedAt) }
@@ -115,9 +115,9 @@ class StatisticsRestController(
@GetMapping(path = ["requestpatientstates"])
fun requestPatientStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> {
val states = if (delete) {
- requestRepository.findPatientUniqueDeleteStates()
+ requestService.findPatientUniqueDeleteStates()
} else {
- requestRepository.findPatientUniqueStates()
+ requestService.findPatientUniqueStates()
}
return states.map {
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 3d4827c..895f026 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -3,17 +3,34 @@ spring:
compose:
file: ./dev-compose.yml
+ security:
+ oauth2:
+ client:
+ registration:
+ custom:
+ client-name: App-Dev
+ client-id: app-dev
+ client-secret: very-secret-ae3f7a-5a9f-1190
+ scope:
+ - openid
+ provider:
+ custom:
+ issuer-uri: https://dnpm.dev/auth/realms/intern
+ user-name-attribute: name
+
app:
- #rest:
- # uri: http://localhost:9000/bwhc/etl/api
- kafka:
- topic: test
- response-topic: test_response
- servers: localhost:9094
- #security:
- # admin-user: admin
- # admin-password: "{noop}very-secret"
+ rest:
+ uri: http://localhost:9000/bwhc/etl/api
+ #kafka:
+ # topic: test
+ # response-topic: test_response
+ # servers: localhost:9094
+ security:
+ admin-user: admin
+ admin-password: "{noop}very-secret"
+ enable-oidc: "true"
server:
port: 8000
+
diff --git a/src/main/resources/db/migration/mariadb/V0_4_0__RenamePatientPseudonym.sql b/src/main/resources/db/migration/mariadb/V0_4_0__RenamePatientPseudonym.sql
new file mode 100644
index 0000000..bb2b0cc
--- /dev/null
+++ b/src/main/resources/db/migration/mariadb/V0_4_0__RenamePatientPseudonym.sql
@@ -0,0 +1 @@
+ALTER TABLE request RENAME COLUMN patient_id TO patient_pseudonym; \ No newline at end of file
diff --git a/src/main/resources/db/migration/postgresql/V0_4_0__RenamePatientPseudonym.sql b/src/main/resources/db/migration/postgresql/V0_4_0__RenamePatientPseudonym.sql
new file mode 100644
index 0000000..bb2b0cc
--- /dev/null
+++ b/src/main/resources/db/migration/postgresql/V0_4_0__RenamePatientPseudonym.sql
@@ -0,0 +1 @@
+ALTER TABLE request RENAME COLUMN patient_id TO patient_pseudonym; \ No newline at end of file
diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css
index 1dd68ed..7066e2b 100644
--- a/src/main/resources/static/style.css
+++ b/src/main/resources/static/style.css
@@ -22,6 +22,10 @@
--bg-gray-op: rgba(112, 128, 144, .35);
}
+* {
+ font-family: sans-serif;
+}
+
html {
background: linear-gradient(-5deg, var(--bg-blue-op), transparent 10em);
min-height: 100vh;
@@ -30,7 +34,6 @@ html {
body {
margin: 0 0 5em 0;
- font-family: sans-serif;
font-size: .8rem;
color: var(--text);
@@ -619,6 +622,10 @@ input.inline:focus-visible {
text-align: center;
}
+.notification.info {
+ color: var(--bg-blue);
+}
+
.notification.success {
color: var(--bg-green);
}
@@ -651,6 +658,7 @@ input.inline:focus-visible {
border-radius: 0 .5em .5em .5em;
display: none;
padding: 1em;
+ background: white;
}
.tabcontent.active {
diff --git a/src/main/resources/templates/configs/gPasConnectionAvailable.html b/src/main/resources/templates/configs/gPasConnectionAvailable.html
index 6dccc60..a9a8517 100644
--- a/src/main/resources/templates/configs/gPasConnectionAvailable.html
+++ b/src/main/resources/templates/configs/gPasConnectionAvailable.html
@@ -2,15 +2,20 @@
<h2><span>🟦</span> gPAS nicht konfiguriert - Patienten-IDs werden intern anonymisiert</h2>
</th:block>
<th:block th:if="${gPasConnectionAvailable != null}">
- <h2><span th:if="${gPasConnectionAvailable}">✅</span><span th:if="${not(gPasConnectionAvailable)}">⚡</span> Verbindung zu gPAS</h2>
+ <h2><span th:if="${gPasConnectionAvailable.available}">✅</span><span th:if="${not(gPasConnectionAvailable.available)}">⚡</span> Verbindung zu gPAS</h2>
<div>
- Die Verbindung ist aktuell
- <strong th:if="${gPasConnectionAvailable}" style="color: green">verfügbar.</strong>
- <strong th:if="${not(gPasConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
+ Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}"></time>
+ &nbsp;|&nbsp;
+ Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}"></time>
+ </div>
+ <div>
+ <span>Die Verbindung ist aktuell</span>
+ <strong th:if="${gPasConnectionAvailable.available}" style="color: green">verfügbar.</strong>
+ <strong th:if="${not(gPasConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
</div>
<div class="connection-display border">
<img th:src="@{/server.png}" alt="ETL-Processor" />
- <span class="connection" th:classappend="${gPasConnectionAvailable ? 'available' : ''}"></span>
+ <span class="connection" th:classappend="${gPasConnectionAvailable.available ? 'available' : ''}"></span>
<img th:src="@{/server.png}" alt="gPAS" />
<span>ETL-Processor</span>
<span></span>
diff --git a/src/main/resources/templates/configs/outputConnectionAvailable.html b/src/main/resources/templates/configs/outputConnectionAvailable.html
index 2b18b75..4b7f8d1 100644
--- a/src/main/resources/templates/configs/outputConnectionAvailable.html
+++ b/src/main/resources/templates/configs/outputConnectionAvailable.html
@@ -1,16 +1,26 @@
-<h2><span th:if="${outputConnectionAvailable}">✅</span><span th:if="${not(outputConnectionAvailable)}">⚡</span> MTB-File Verbindung</h2>
-<div>
- Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
- <strong th:if="${outputConnectionAvailable}" style="color: green">verfügbar.</strong>
- <strong th:if="${not(outputConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
-</div>
-<div class="connection-display border">
- <img th:src="@{/server.png}" alt="ETL-Processor" />
- <span class="connection" th:classappend="${outputConnectionAvailable ? 'available' : ''}"></span>
- <img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
- <img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
- <span>ETL-Processor</span>
- <span></span>
- <span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
- <span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
-</div> \ No newline at end of file
+<th:block th:if="${outputConnectionAvailable == null}">
+ <h2><span>🟦</span> Keine Ausgabenkonfiguration</h2>
+</th:block>
+<th:block th:if="${outputConnectionAvailable != null}">
+ <h2><span th:if="${outputConnectionAvailable.available}">✅</span><span th:if="${not(outputConnectionAvailable.available)}">⚡</span> MTB-File Verbindung</h2>
+ <div>
+ Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(outputConnectionAvailable.timestamp)}"></time>
+ &nbsp;|&nbsp;
+ Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(outputConnectionAvailable.lastChange)}"></time>
+ </div>
+ <div>
+ Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
+ <strong th:if="${outputConnectionAvailable.available}" style="color: green">verfügbar.</strong>
+ <strong th:if="${not(outputConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
+ </div>
+ <div class="connection-display border">
+ <img th:src="@{/server.png}" alt="ETL-Processor" />
+ <span class="connection" th:classappend="${outputConnectionAvailable.available ? 'available' : ''}"></span>
+ <img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
+ <img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
+ <span>ETL-Processor</span>
+ <span></span>
+ <span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
+ <span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
+ </div>
+</th:block> \ No newline at end of file
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index be3123b..7ca0b67 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -12,26 +12,30 @@
<h1>Alle Anfragen<a id="reload-notify" class="reload" title="Neue Anfragen" th:href="@{/}">⟳</a></h1>
<div>
- <h2 th:if="${patientId != null}">
- Betreffend Patienten-Pseudonym <span class="monospace" th:text="${patientId}">***</span>
- <a class="btn btn-blue" th:if="${patientId != null}" th:href="@{/}">Alle anzeigen</a>
+ <h2 th:if="${patientPseudonym != null}">
+ Betreffend Patienten-Pseudonym <span class="monospace" th:text="${patientPseudonym}">***</span>
+ <a class="btn btn-blue" th:if="${patientPseudonym != null}" th:href="@{/}">Alle anzeigen</a>
</h2>
</div>
- <div class="border">
- <div th:if="${patientId == null}" class="page-control">
+ <div class="border" th:if="${requests.totalElements == 0}">
+ <div class="notification info">Noch keine Anfragen eingegangen</div>
+ </div>
+
+ <div class="border" th:if="${requests.totalElements > 0}">
+ <div th:if="${patientPseudonym == null}" class="page-control">
<a id="first-page-link" th:href="@{/(page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">&larrb;</a><a th:if="${requests.isFirst()}">&larrb;</a>
<a id="prev-page-link" th:href="@{/(page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">&larr;</a><a th:if="${requests.isFirst()}">&larr;</a>
<span>Seite [[ ${requests.getNumber() + 1} ]] von [[ ${requests.getTotalPages()} ]]</span>
<a id="next-page-link" th:href="@{/(page=${requests.getNumber() + 1})}" title="Seite vor: Taste D" th:if="${not requests.isLast()}">&rarr;</a><a th:if="${requests.isLast()}">&rarr;</a>
<a id="last-page-link" th:href="@{/(page=${requests.getTotalPages() - 1})}" title="Zum Ende: Taste S" th:if="${not requests.isLast()}">&rarrb;</a><a th:if="${requests.isLast()}">&rarrb;</a>
</div>
- <div th:if="${patientId != null}" class="page-control">
- <a id="first-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">&larrb;</a><a th:if="${requests.isFirst()}">&larrb;</a>
- <a id="prev-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">&larr;</a><a th:if="${requests.isFirst()}">&larr;</a>
+ <div th:if="${patientPseudonym != null}" class="page-control">
+ <a id="first-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${0})}" title="Zum Anfang: Taste W" th:if="${not requests.isFirst()}">&larrb;</a><a th:if="${requests.isFirst()}">&larrb;</a>
+ <a id="prev-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${requests.getNumber() - 1})}" title="Seite zurück: Taste A" th:if="${not requests.isFirst()}">&larr;</a><a th:if="${requests.isFirst()}">&larr;</a>
<span>Seite [[ ${requests.getNumber() + 1} ]] von [[ ${requests.getTotalPages()} ]]</span>
- <a id="next-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${requests.getNumber() + 1})}" title="Seite vor: Taste D" th:if="${not requests.isLast()}">&rarr;</a><a th:if="${requests.isLast()}">&rarr;</a>
- <a id="last-page-link" th:href="@{/patient/{patientId}(patientId=${patientId},page=${requests.getTotalPages() - 1})}" title="Zum Ende: Taste S" th:if="${not requests.isLast()}">&rarrb;</a><a th:if="${requests.isLast()}">&rarrb;</a>
+ <a id="next-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${requests.getNumber() + 1})}" title="Seite vor: Taste D" th:if="${not requests.isLast()}">&rarr;</a><a th:if="${requests.isLast()}">&rarr;</a>
+ <a id="last-page-link" th:href="@{/patient/{patientPseudonym}(patientPseudonym=${patientPseudonym},page=${requests.getTotalPages() - 1})}" title="Zum Ende: Taste S" th:if="${not requests.isLast()}">&rarrb;</a><a th:if="${requests.isLast()}">&rarrb;</a>
</div>
<table class="paged">
<thead>
@@ -57,11 +61,11 @@
<th:block sec:authorize="not (hasRole('USER') or hasRole('ADMIN'))">[[ ${request.uuid} ]]</th:block>
</td>
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td>
- <td class="patient-id" th:if="${patientId != null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
- [[ ${request.patientId} ]]
+ <td class="patient-id" th:if="${patientPseudonym != null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
+ [[ ${request.patientPseudonym} ]]
</td>
- <td class="patient-id" th:if="${patientId == null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
- <a th:href="@{/patient/{pid}(pid=${request.patientId})}">[[ ${request.patientId} ]]</a>
+ <td class="patient-id" th:if="${patientPseudonym == null}" sec:authorize="hasRole('USER') or hasRole('ADMIN')">
+ <a th:href="@{/patient/{pid}(pid=${request.patientPseudonym})}">[[ ${request.patientPseudonym} ]]</a>
</td>
<td class="patient-id" sec:authorize="not (hasRole('USER') or hasRole('ADMIN'))">***</td>
</tr>
diff --git a/src/main/resources/templates/report.html b/src/main/resources/templates/report.html
index 07f987c..21d1b48 100644
--- a/src/main/resources/templates/report.html
+++ b/src/main/resources/templates/report.html
@@ -31,7 +31,7 @@
<td th:style="${request.type.value == 'delete'} ? 'color: red;'"><small>[[ ${request.type} ]]</small></td>
<td>[[ ${request.uuid} ]]</td>
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td>
- <td class="patient-id" sec:authorize="authenticated">[[ ${request.patientId} ]]</td>
+ <td class="patient-id" sec:authorize="authenticated">[[ ${request.patientPseudonym} ]]</td>
<td class="patient-id" sec:authorize="not authenticated">***</td>
</tr>
</tbody>
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt b/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt
new file mode 100644
index 0000000..55d6327
--- /dev/null
+++ b/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt
@@ -0,0 +1,29 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor
+
+import org.mockito.ArgumentMatchers
+
+inline fun <reified T> anyValueClass(): T {
+ val unboxedClass = T::class.java.declaredFields.first().type
+ return ArgumentMatchers.any(unboxedClass as Class<T>)
+ ?: T::class.java.getDeclaredMethod("box-impl", unboxedClass)
+ .invoke(null, null) as T
+} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt
index 1157644..7753dbc 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
import de.ukw.ccc.bwhc.dto.Patient
+import dev.dnpm.etl.processor.anyValueClass
import dev.dnpm.etl.processor.services.RequestProcessor
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.header.internals.RecordHeader
@@ -31,7 +32,6 @@ import org.apache.kafka.common.record.TimestampType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
@@ -77,7 +77,7 @@ class KafkaInputListenerTest {
kafkaInputListener.onMessage(ConsumerRecord("testtopic", 0, 0, "", this.objectMapper.writeValueAsString(mtbFile)))
- verify(requestProcessor, times(1)).processDeletion(anyString())
+ verify(requestProcessor, times(1)).processDeletion(anyValueClass())
}
@Test
@@ -92,7 +92,7 @@ class KafkaInputListenerTest {
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
)
- verify(requestProcessor, times(1)).processMtbFile(any(), anyString())
+ verify(requestProcessor, times(1)).processMtbFile(any(), anyValueClass())
}
@Test
@@ -106,7 +106,7 @@ class KafkaInputListenerTest {
kafkaInputListener.onMessage(
ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty())
)
- verify(requestProcessor, times(1)).processDeletion(anyString(), anyString())
+ verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass())
}
} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
index 0b076a1..f9fe3f3 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
@@ -21,8 +21,8 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
+import dev.dnpm.etl.processor.anyValueClass
import dev.dnpm.etl.processor.services.RequestProcessor
-import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -31,7 +31,6 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
-import org.mockito.kotlin.argumentCaptor
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.delete
@@ -129,9 +128,7 @@ class MtbFileRestControllerTest {
}
}
- val captor = argumentCaptor<String>()
- verify(requestProcessor, times(1)).processDeletion(captor.capture())
- assertThat(captor.firstValue).isEqualTo("TEST_12345678")
+ verify(requestProcessor, times(1)).processDeletion(anyValueClass())
}
@Test
@@ -142,9 +139,7 @@ class MtbFileRestControllerTest {
}
}
- val captor = argumentCaptor<String>()
- verify(requestProcessor, times(1)).processDeletion(captor.capture())
- assertThat(captor.firstValue).isEqualTo("TEST_12345678")
+ verify(requestProcessor, times(1)).processDeletion(anyValueClass())
}
} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt
index 411c51e..655e29e 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt
@@ -21,6 +21,8 @@ package dev.dnpm.etl.processor.output
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
+import dev.dnpm.etl.processor.PatientPseudonym
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.config.KafkaProperties
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.assertj.core.api.Assertions.assertThat
@@ -72,7 +74,7 @@ class KafkaMtbFileSenderTest {
completedFuture(SendResult<String, String>(null, null))
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
- val response = kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile(Consent.Status.ACTIVE)))
+ val response = kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
assertThat(response.status).isEqualTo(testData.requestStatus)
}
@@ -86,7 +88,7 @@ class KafkaMtbFileSenderTest {
completedFuture(SendResult<String, String>(null, null))
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
- val response = kafkaMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
+ val response = kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
assertThat(response.status).isEqualTo(testData.requestStatus)
}
@@ -96,14 +98,14 @@ class KafkaMtbFileSenderTest {
completedFuture(SendResult<String, String>(null, null))
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
- kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile(Consent.Status.ACTIVE)))
+ 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("TestID", Consent.Status.ACTIVE)))
+ assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.ACTIVE)))
}
@Test
@@ -112,14 +114,14 @@ class KafkaMtbFileSenderTest {
completedFuture(SendResult<String, String>(null, null))
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
- kafkaMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
+ 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("TestID", Consent.Status.REJECTED)))
+ assertThat(captor.secondValue).isEqualTo(objectMapper.writeValueAsString(kafkaRecordData(TEST_REQUEST_ID, Consent.Status.REJECTED)))
}
@ParameterizedTest
@@ -136,7 +138,7 @@ class KafkaMtbFileSenderTest {
completedFuture(SendResult<String, String>(null, null))
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
- kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile(Consent.Status.ACTIVE)))
+ kafkaMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile(Consent.Status.ACTIVE)))
val expectedCount = when (testData.exception) {
// OK - No Retry
@@ -162,7 +164,7 @@ class KafkaMtbFileSenderTest {
completedFuture(SendResult<String, String>(null, null))
}.whenever(kafkaTemplate).send(anyString(), anyString(), anyString())
- kafkaMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
+ kafkaMtbFileSender.send(MtbFileSender.DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM))
val expectedCount = when (testData.exception) {
// OK - No Retry
@@ -175,6 +177,9 @@ class KafkaMtbFileSenderTest {
}
companion object {
+ val TEST_REQUEST_ID = RequestId("TestId")
+ val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
+
fun mtbFile(consentStatus: Consent.Status): MtbFile {
return if (consentStatus == Consent.Status.ACTIVE) {
MtbFile.builder()
@@ -210,7 +215,7 @@ class KafkaMtbFileSenderTest {
}.build()
}
- fun kafkaRecordData(requestId: String, consentStatus: Consent.Status): KafkaMtbFileSender.Data {
+ fun kafkaRecordData(requestId: RequestId, consentStatus: Consent.Status): KafkaMtbFileSender.Data {
return KafkaMtbFileSender.Data(requestId, mtbFile(consentStatus))
}
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSenderTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSenderTest.kt
index df19ddb..9b6332a 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSenderTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSenderTest.kt
@@ -20,6 +20,8 @@
package dev.dnpm.etl.processor.output
import de.ukw.ccc.bwhc.dto.*
+import dev.dnpm.etl.processor.PatientPseudonym
+import dev.dnpm.etl.processor.RequestId
import dev.dnpm.etl.processor.config.RestTargetProperties
import dev.dnpm.etl.processor.monitoring.RequestStatus
import org.assertj.core.api.Assertions.assertThat
@@ -46,7 +48,7 @@ class RestMtbFileSenderTest {
@BeforeEach
fun setup() {
val restTemplate = RestTemplate()
- val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile")
+ val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
@@ -64,7 +66,7 @@ class RestMtbFileSenderTest {
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
}
- val response = restMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
+ 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)
}
@@ -79,7 +81,7 @@ class RestMtbFileSenderTest {
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
}
- val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile))
+ val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
}
@@ -88,7 +90,7 @@ class RestMtbFileSenderTest {
@MethodSource("mtbFileRequestWithResponseSource")
fun shouldRetryOnMtbFileHttpRequestError(requestWithResponse: RequestWithResponse) {
val restTemplate = RestTemplate()
- val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile")
+ val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
@@ -108,7 +110,7 @@ class RestMtbFileSenderTest {
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
}
- val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest("TestID", mtbFile))
+ val response = restMtbFileSender.send(MtbFileSender.MtbFileRequest(TEST_REQUEST_ID, mtbFile))
assertThat(response.status).isEqualTo(requestWithResponse.response.status)
assertThat(response.body).isEqualTo(requestWithResponse.response.body)
}
@@ -117,7 +119,7 @@ class RestMtbFileSenderTest {
@MethodSource("deleteRequestWithResponseSource")
fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) {
val restTemplate = RestTemplate()
- val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile")
+ val restTargetProperties = RestTargetProperties("http://localhost:9000/mtbfile", null, null)
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build()
this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
@@ -137,7 +139,7 @@ class RestMtbFileSenderTest {
withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it)
}
- val response = restMtbFileSender.send(MtbFileSender.DeleteRequest("TestID", "PID"))
+ 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)
}
@@ -149,6 +151,9 @@ class RestMtbFileSenderTest {
val response: MtbFileSender.Response
)
+ val TEST_REQUEST_ID = RequestId("TestId")
+ val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID")
+
private val warningBody = """
{
"patient_id": "PID",
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt
index d8c7813..fbc26ae 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt
@@ -21,11 +21,11 @@ package dev.dnpm.etl.processor.pseudonym
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
+import dev.dnpm.etl.processor.anyValueClass
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
-import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.doAnswer
@@ -52,7 +52,7 @@ class ExtensionsTest {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
- }.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
val mtbFile = fakeMtbFile()
@@ -67,7 +67,7 @@ class ExtensionsTest {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
- }.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
"TESTDOMAIN"
@@ -95,7 +95,7 @@ class ExtensionsTest {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
- }.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
"TESTDOMAIN"
@@ -139,7 +139,7 @@ class ExtensionsTest {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
- }.whenever(pseudonymizeService).patientPseudonym(ArgumentMatchers.anyString())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
"TESTDOMAIN"
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/TokenServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt
index 1fdc3d9..b93e9f5 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/TokenServiceTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt
@@ -17,13 +17,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package dev.dnpm.etl.processor.services
+package dev.dnpm.etl.processor.security
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
@@ -96,11 +95,11 @@ class TokenServiceTest {
val actual = this.tokenService.addToken("Test Token")
- val captor = ArgumentCaptor.forClass(Token::class.java)
+ val captor = argumentCaptor<Token>()
verify(tokenRepository, times(1)).save(captor.capture())
assertThat(actual).satisfies(Consumer { assertThat(it.isSuccess).isTrue() })
- assertThat(captor.value).satisfies(
+ assertThat(captor.firstValue).satisfies(
Consumer { assertThat(it.name).isEqualTo("Test Token") },
Consumer { assertThat(it.username).isEqualTo("testtoken") },
Consumer { assertThat(it.password).isEqualTo("{test}verysecret") }
@@ -116,13 +115,13 @@ class TokenServiceTest {
this.tokenService.deleteToken(42)
- val stringCaptor = ArgumentCaptor.forClass(String::class.java)
+ val stringCaptor = argumentCaptor<String>()
verify(userDetailsManager, times(1)).deleteUser(stringCaptor.capture())
- assertThat(stringCaptor.value).isEqualTo("testtoken")
+ assertThat(stringCaptor.firstValue).isEqualTo("testtoken")
- val tokenCaptor = ArgumentCaptor.forClass(Token::class.java)
+ val tokenCaptor = argumentCaptor<Token>()
verify(tokenRepository, times(1)).delete(tokenCaptor.capture())
- assertThat(tokenCaptor.value.id).isEqualTo(42)
+ assertThat(tokenCaptor.firstValue.id).isEqualTo(42)
}
@Test
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt
new file mode 100644
index 0000000..39ba7c0
--- /dev/null
+++ b/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt
@@ -0,0 +1,202 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.security
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.Mock
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.*
+import org.springframework.security.core.session.SessionInformation
+import org.springframework.security.core.session.SessionRegistry
+import org.springframework.security.oauth2.core.oidc.OidcIdToken
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser
+import org.springframework.security.oauth2.core.oidc.user.OidcUser
+import java.time.Instant
+import java.util.*
+
+@ExtendWith(MockitoExtension::class)
+class UserRoleServiceTest {
+
+ private lateinit var userRoleRepository: UserRoleRepository
+ private lateinit var sessionRegistry: SessionRegistry
+
+ private lateinit var userRoleService: UserRoleService
+
+ @BeforeEach
+ fun setup(
+ @Mock userRoleRepository: UserRoleRepository,
+ @Mock sessionRegistry: SessionRegistry
+ ) {
+ this.userRoleRepository = userRoleRepository
+ this.sessionRegistry = sessionRegistry
+
+ this.userRoleService = UserRoleService(userRoleRepository, sessionRegistry)
+ }
+
+ @Test
+ fun shouldDelegateFindAllToRepository() {
+ userRoleService.findAll()
+
+ verify(userRoleRepository, times(1)).findAll()
+ }
+
+ @Nested
+ inner class WithExistingUserRole {
+
+ @BeforeEach
+ fun setup() {
+ doAnswer { invocation ->
+ Optional.of(
+ UserRole(invocation.getArgument(0), "patrick.tester", Role.USER)
+ )
+ }.whenever(userRoleRepository).findById(any<Long>())
+
+ doAnswer { _ ->
+ listOf(
+ dummyPrincipal()
+ )
+ }.whenever(sessionRegistry).allPrincipals
+ }
+
+ @Test
+ fun shouldUpdateUserRole() {
+ userRoleService.updateUserRole(1, Role.ADMIN)
+
+ val userRoleCaptor = argumentCaptor<UserRole>()
+ verify(userRoleRepository, times(1)).save(userRoleCaptor.capture())
+
+ assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L)
+ assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.ADMIN)
+ }
+
+ @Test
+ fun shouldExpireSessionOnUpdate() {
+ val dummySessions = dummySessions()
+ whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn(
+ dummySessions
+ )
+
+ assertThat(dummySessions.filter { it.isExpired }).hasSize(0)
+
+ userRoleService.updateUserRole(1, Role.ADMIN)
+
+ verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>())
+
+ assertThat(dummySessions.filter { it.isExpired }).hasSize(2)
+ }
+
+ @Test
+ fun shouldDeleteUserRole() {
+ userRoleService.deleteUserRole(1)
+
+ val userRoleCaptor = argumentCaptor<UserRole>()
+ verify(userRoleRepository, times(1)).delete(userRoleCaptor.capture())
+
+ assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L)
+ assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.USER)
+ }
+
+ @Test
+ fun shouldExpireSessionOnDelete() {
+ val dummySessions = dummySessions()
+ whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn(
+ dummySessions
+ )
+
+ assertThat(dummySessions.filter { it.isExpired }).hasSize(0)
+
+ userRoleService.deleteUserRole(1)
+
+ verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>())
+
+ assertThat(dummySessions.filter { it.isExpired }).hasSize(2)
+ }
+ }
+
+ @Nested
+ inner class WithoutExistingUserRole {
+
+ @BeforeEach
+ fun setup() {
+ doAnswer { _ ->
+ Optional.empty<UserRole>()
+ }.whenever(userRoleRepository).findById(any<Long>())
+ }
+
+ @Test
+ fun shouldNotUpdateUserRole() {
+ userRoleService.updateUserRole(1, Role.ADMIN)
+
+ verify(userRoleRepository, never()).save(any<UserRole>())
+ }
+
+ @Test
+ fun shouldNotExpireSessionOnUpdate() {
+ userRoleService.updateUserRole(1, Role.ADMIN)
+
+ verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>())
+ }
+
+ @Test
+ fun shouldNotDeleteUserRole() {
+ userRoleService.deleteUserRole(1)
+
+ verify(userRoleRepository, never()).delete(any<UserRole>())
+ }
+
+ @Test
+ fun shouldNotExpireSessionOnDelete() {
+ userRoleService.deleteUserRole(1)
+
+ verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>())
+ }
+
+ }
+
+
+ companion object {
+ private fun dummyPrincipal() = DefaultOidcUser(
+ listOf(),
+ OidcIdToken(
+ "anytokenvalue",
+ Instant.now(),
+ Instant.now().plusSeconds(10),
+ mapOf("sub" to "testsub", "preferred_username" to "patrick.tester")
+ )
+ )
+
+ private fun dummySessions() = listOf(
+ SessionInformation(
+ dummyPrincipal(),
+ "SESSIONID1",
+ Date.from(Instant.now()),
+ ),
+ SessionInformation(
+ dummyPrincipal(),
+ "SESSIONID2",
+ Date.from(Instant.now()),
+ )
+ )
+ }
+} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt
index 611c0ff..1c58d5d 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt
@@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.services
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
+import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestStatus
@@ -32,7 +33,6 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.junit.jupiter.MockitoExtension
@@ -41,7 +41,6 @@ import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.whenever
import org.springframework.context.ApplicationEventPublisher
import java.time.Instant
-import java.util.*
@ExtendWith(MockitoExtension::class)
@@ -88,24 +87,24 @@ class RequestProcessorTest {
fun testShouldSendMtbFileDuplicationAndSaveUnknownRequestStatusAtFirst() {
doAnswer {
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga",
- type = RequestType.MTB_FILE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-08-08T02:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-08-08T02:00:00Z")
)
- }.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
+ }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
doAnswer {
false
- }.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
+ }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
doAnswer {
it.arguments[0] as String
- }.`when`(pseudonymizeService).patientPseudonym(any())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
it.arguments[0]
@@ -147,24 +146,24 @@ class RequestProcessorTest {
fun testShouldDetectMtbFileDuplicationAndSendDuplicationEvent() {
doAnswer {
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga",
- type = RequestType.MTB_FILE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-08-08T02:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("zdlzv5s5ydmd4ktw2v5piohegc4jcyrm6j66bq6tv2uxuerndmga"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-08-08T02:00:00Z")
)
- }.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
+ }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
doAnswer {
false
- }.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
+ }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
doAnswer {
it.arguments[0] as String
- }.`when`(pseudonymizeService).patientPseudonym(any())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
it.arguments[0]
@@ -206,28 +205,28 @@ class RequestProcessorTest {
fun testShouldSendMtbFileAndSendSuccessEvent() {
doAnswer {
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "different",
- type = RequestType.MTB_FILE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-08-08T02:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("different"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-08-08T02:00:00Z")
)
- }.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
+ }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
doAnswer {
false
- }.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
+ }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
doAnswer {
MtbFileSender.Response(status = RequestStatus.SUCCESS)
- }.`when`(sender).send(any<MtbFileSender.MtbFileRequest>())
+ }.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
doAnswer {
it.arguments[0] as String
- }.`when`(pseudonymizeService).patientPseudonym(any())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
it.arguments[0]
@@ -269,28 +268,28 @@ class RequestProcessorTest {
fun testShouldSendMtbFileAndSendErrorEvent() {
doAnswer {
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "different",
- type = RequestType.MTB_FILE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-08-08T02:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("different"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-08-08T02:00:00Z")
)
- }.`when`(requestService).lastMtbFileRequestForPatientPseudonym(anyString())
+ }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass())
doAnswer {
false
- }.`when`(requestService).isLastRequestWithKnownStatusDeletion(anyString())
+ }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass())
doAnswer {
MtbFileSender.Response(status = RequestStatus.ERROR)
- }.`when`(sender).send(any<MtbFileSender.MtbFileRequest>())
+ }.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
doAnswer {
it.arguments[0] as String
- }.`when`(pseudonymizeService).patientPseudonym(any())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
it.arguments[0]
@@ -332,13 +331,13 @@ class RequestProcessorTest {
fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() {
doAnswer {
"PSEUDONYM"
- }.`when`(pseudonymizeService).patientPseudonym(anyString())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
MtbFileSender.Response(status = RequestStatus.UNKNOWN)
- }.`when`(sender).send(any<MtbFileSender.DeleteRequest>())
+ }.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
- this.requestProcessor.processDeletion("TEST_12345678901")
+ this.requestProcessor.processDeletion(TEST_PATIENT_ID)
val requestCaptor = argumentCaptor<Request>()
verify(requestService, times(1)).save(requestCaptor.capture())
@@ -350,13 +349,13 @@ class RequestProcessorTest {
fun testShouldSendDeleteRequestAndSendSuccessEvent() {
doAnswer {
"PSEUDONYM"
- }.`when`(pseudonymizeService).patientPseudonym(anyString())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
MtbFileSender.Response(status = RequestStatus.SUCCESS)
- }.`when`(sender).send(any<MtbFileSender.DeleteRequest>())
+ }.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
- this.requestProcessor.processDeletion("TEST_12345678901")
+ this.requestProcessor.processDeletion(TEST_PATIENT_ID)
val eventCaptor = argumentCaptor<ResponseEvent>()
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
@@ -368,13 +367,13 @@ class RequestProcessorTest {
fun testShouldSendDeleteRequestAndSendErrorEvent() {
doAnswer {
"PSEUDONYM"
- }.`when`(pseudonymizeService).patientPseudonym(anyString())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
MtbFileSender.Response(status = RequestStatus.ERROR)
- }.`when`(sender).send(any<MtbFileSender.DeleteRequest>())
+ }.whenever(sender).send(any<MtbFileSender.DeleteRequest>())
- this.requestProcessor.processDeletion("TEST_12345678901")
+ this.requestProcessor.processDeletion(TEST_PATIENT_ID)
val eventCaptor = argumentCaptor<ResponseEvent>()
verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture())
@@ -384,9 +383,9 @@ class RequestProcessorTest {
@Test
fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() {
- doThrow(RuntimeException()).`when`(pseudonymizeService).patientPseudonym(anyString())
+ doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass())
- this.requestProcessor.processDeletion("TEST_12345678901")
+ this.requestProcessor.processDeletion(TEST_PATIENT_ID)
val requestCaptor = argumentCaptor<Request>()
verify(requestService, times(1)).save(requestCaptor.capture())
@@ -400,7 +399,7 @@ class RequestProcessorTest {
doAnswer {
it.arguments[0] as String
- }.`when`(pseudonymizeService).patientPseudonym(any())
+ }.whenever(pseudonymizeService).patientPseudonym(anyValueClass())
doAnswer {
it.arguments[0]
@@ -408,7 +407,7 @@ class RequestProcessorTest {
doAnswer {
MtbFileSender.Response(status = RequestStatus.SUCCESS)
- }.`when`(sender).send(any<MtbFileSender.MtbFileRequest>())
+ }.whenever(sender).send(any<MtbFileSender.MtbFileRequest>())
val mtbFile = MtbFile.builder()
.withPatient(
@@ -442,4 +441,8 @@ class RequestProcessorTest {
assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS)
}
+ companion object {
+ val TEST_PATIENT_ID = PatientId("TEST_12345678901")
+ }
+
} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt
index 3cf8804..2e289c5 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt
@@ -19,6 +19,7 @@
package dev.dnpm.etl.processor.services
+import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.monitoring.Request
import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
@@ -30,8 +31,8 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.whenever
import java.time.Instant
-import java.util.*
@ExtendWith(MockitoExtension::class)
class RequestServiceTest {
@@ -41,14 +42,14 @@ class RequestServiceTest {
private lateinit var requestService: RequestService
private fun anyRequest() = any(Request::class.java) ?: Request(
- id = 0L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_dummy",
- pid = "PX",
- fingerprint = "dummy",
- type = RequestType.MTB_FILE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-08-08T02:00:00Z")
+ 0L,
+ randomRequestId(),
+ PatientPseudonym("TEST_dummy"),
+ PatientId("PX"),
+ Fingerprint("dummy"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-08-08T02:00:00Z")
)
@BeforeEach
@@ -63,34 +64,34 @@ class RequestServiceTest {
fun shouldIndicateLastRequestIsDeleteRequest() {
val requests = listOf(
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.MTB_FILE,
- status = RequestStatus.WARNING,
- processedAt = Instant.parse("2023-07-07T00:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-07-07T00:00:00Z")
),
Request(
- id = 2L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdefd",
- type = RequestType.DELETE,
- status = RequestStatus.WARNING,
- processedAt = Instant.parse("2023-07-07T02:00:00Z")
+ 2L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdefd"),
+ RequestType.DELETE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-07-07T02:00:00Z")
),
Request(
- id = 3L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.MTB_FILE,
- status = RequestStatus.UNKNOWN,
- processedAt = Instant.parse("2023-08-11T00:00:00Z")
+ 3L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.UNKNOWN,
+ Instant.parse("2023-08-11T00:00:00Z")
)
)
@@ -103,34 +104,34 @@ class RequestServiceTest {
fun shouldIndicateLastRequestIsNotDeleteRequest() {
val requests = listOf(
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.MTB_FILE,
- status = RequestStatus.WARNING,
- processedAt = Instant.parse("2023-07-07T00:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-07-07T00:00:00Z")
),
Request(
- id = 2L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.MTB_FILE,
- status = RequestStatus.WARNING,
- processedAt = Instant.parse("2023-07-07T02:00:00Z")
+ 2L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-07-07T02:00:00Z")
),
Request(
- id = 3L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.MTB_FILE,
- status = RequestStatus.UNKNOWN,
- processedAt = Instant.parse("2023-08-11T00:00:00Z")
+ 3L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.UNKNOWN,
+ Instant.parse("2023-08-11T00:00:00Z")
)
)
@@ -143,31 +144,31 @@ class RequestServiceTest {
fun shouldReturnPatientsLastRequest() {
val requests = listOf(
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.DELETE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-07-07T02:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.DELETE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-07-07T02:00:00Z")
),
Request(
- id = 1L,
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678902",
- pid = "P2",
- fingerprint = "0123456789abcdef2",
- type = RequestType.MTB_FILE,
- status = RequestStatus.WARNING,
- processedAt = Instant.parse("2023-08-08T00:00:00Z")
+ 1L,
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678902"),
+ PatientId("P2"),
+ Fingerprint("0123456789abcdef2"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-08-08T00:00:00Z")
)
)
val actual = RequestService.lastMtbFileRequestForPatientPseudonym(requests)
assertThat(actual).isInstanceOf(Request::class.java)
- assertThat(actual?.fingerprint).isEqualTo("0123456789abcdef2")
+ assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef2"))
}
@Test
@@ -184,16 +185,16 @@ class RequestServiceTest {
doAnswer {
val obj = it.arguments[0] as Request
obj.copy(id = 1L)
- }.`when`(requestRepository).save(anyRequest())
+ }.whenever(requestRepository).save(anyRequest())
val request = Request(
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.DELETE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-07-07T02:00:00Z")
+ randomRequestId(),
+ PatientPseudonym("TEST_12345678901"),
+ PatientId("P1"),
+ Fingerprint("0123456789abcdef1"),
+ RequestType.DELETE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-07-07T02:00:00Z")
)
requestService.save(request)
@@ -203,23 +204,23 @@ class RequestServiceTest {
@Test
fun allRequestsByPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() {
- requestService.allRequestsByPatientPseudonym("TEST_12345678901")
+ requestService.allRequestsByPatientPseudonym(PatientPseudonym("TEST_12345678901"))
- verify(requestRepository, times(1)).findAllByPatientIdOrderByProcessedAtDesc(anyString())
+ verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass())
}
@Test
fun lastMtbFileRequestForPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() {
- requestService.lastMtbFileRequestForPatientPseudonym("TEST_12345678901")
+ requestService.lastMtbFileRequestForPatientPseudonym(PatientPseudonym("TEST_12345678901"))
- verify(requestRepository, times(1)).findAllByPatientIdOrderByProcessedAtDesc(anyString())
+ verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass())
}
@Test
fun isLastRequestDeletionShouldRequestAllRequestsForPatientPseudonym() {
- requestService.isLastRequestWithKnownStatusDeletion("TEST_12345678901")
+ requestService.isLastRequestWithKnownStatusDeletion(PatientPseudonym("TEST_12345678901"))
- verify(requestRepository, times(1)).findAllByPatientIdOrderByProcessedAtDesc(anyString())
+ verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass())
}
} \ No newline at end of file
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt
index b9e4b7f..465d8b8 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt
@@ -19,8 +19,8 @@
package dev.dnpm.etl.processor.services
+import dev.dnpm.etl.processor.*
import dev.dnpm.etl.processor.monitoring.Request
-import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
import org.assertj.core.api.Assertions.assertThat
@@ -29,7 +29,6 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
-import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.*
@@ -40,64 +39,64 @@ import java.util.*
@ExtendWith(MockitoExtension::class)
class ResponseProcessorTest {
- private lateinit var requestRepository: RequestRepository
+ private lateinit var requestService: RequestService
private lateinit var statisticsUpdateProducer: Sinks.Many<Any>
private lateinit var responseProcessor: ResponseProcessor
private val testRequest = Request(
1L,
- "TestID1234",
- "PSEUDONYM-A",
- "1",
- "dummyfingerprint",
+ RequestId("TestID1234"),
+ PatientPseudonym("PSEUDONYM-A"),
+ PatientId("1"),
+ Fingerprint("dummyfingerprint"),
RequestType.MTB_FILE,
RequestStatus.UNKNOWN
)
@BeforeEach
fun setup(
- @Mock requestRepository: RequestRepository,
+ @Mock requestService: RequestService,
@Mock statisticsUpdateProducer: Sinks.Many<Any>
) {
- this.requestRepository = requestRepository
+ this.requestService = requestService
this.statisticsUpdateProducer = statisticsUpdateProducer
- this.responseProcessor = ResponseProcessor(requestRepository, statisticsUpdateProducer)
+ this.responseProcessor = ResponseProcessor(requestService, statisticsUpdateProducer)
}
@Test
fun shouldNotSaveStatusForUnknownRequest() {
doAnswer {
Optional.empty<Request>()
- }.whenever(requestRepository).findByUuidEquals(anyString())
+ }.whenever(requestService).findByUuid(anyValueClass())
val event = ResponseEvent(
- "TestID1234",
+ RequestId("TestID1234"),
Instant.parse("2023-09-09T00:00:00Z"),
RequestStatus.SUCCESS
)
this.responseProcessor.handleResponseEvent(event)
- verify(requestRepository, never()).save(any())
+ verify(requestService, never()).save(any())
}
@Test
fun shouldNotSaveStatusWithUnknownState() {
doAnswer {
Optional.of(testRequest)
- }.whenever(requestRepository).findByUuidEquals(anyString())
+ }.whenever(requestService).findByUuid(anyValueClass())
val event = ResponseEvent(
- "TestID1234",
+ RequestId("TestID1234"),
Instant.parse("2023-09-09T00:00:00Z"),
RequestStatus.UNKNOWN
)
this.responseProcessor.handleResponseEvent(event)
- verify(requestRepository, never()).save(any())
+ verify(requestService, never()).save(any<Request>())
}
@ParameterizedTest
@@ -105,10 +104,10 @@ class ResponseProcessorTest {
fun shouldSaveStatusForKnownRequest(requestStatus: RequestStatus) {
doAnswer {
Optional.of(testRequest)
- }.whenever(requestRepository).findByUuidEquals(anyString())
+ }.whenever(requestService).findByUuid(anyValueClass())
val event = ResponseEvent(
- "TestID1234",
+ RequestId("TestID1234"),
Instant.parse("2023-09-09T00:00:00Z"),
requestStatus
)
@@ -116,7 +115,7 @@ class ResponseProcessorTest {
this.responseProcessor.handleResponseEvent(event)
val captor = argumentCaptor<Request>()
- verify(requestRepository, times(1)).save(captor.capture())
+ verify(requestService, times(1)).save(captor.capture())
assertThat(captor.firstValue).isNotNull
assertThat(captor.firstValue.status).isEqualTo(requestStatus)
}