summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md35
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt6
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java2
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java93
-rw-r--r--src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java12
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt3
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt15
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt4
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt5
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt2
-rw-r--r--src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt17
-rw-r--r--src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt16
12 files changed, 172 insertions, 38 deletions
diff --git a/README.md b/README.md
index 1307af1..23740ca 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,7 @@ Duplikate werden verworfen, Änderungen werden weitergeleitet.
Löschanfragen werden immer als Löschanfrage an DNPM:DIP weitergeleitet.
-Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand
-der Anwendung gewährt.
+Zudem ist eine minimalistische Weboberfläche integriert, die einen Einblick in den aktuellen Zustand der Anwendung gewährt.
![Modell DNPM-ETL-Strecke](docs/etl.png)
@@ -26,6 +25,18 @@ Konfigurationsparameter
### Modelvorhaben genomDE §64e
+#### Vorgangsummern
+Zusätzlich zur Patienten Identifier Pseudonymisierung müssen Vorgangsummern generiert werden, die
+jede Übertragung eindeutig identifizieren aber gleichzeitig dem Patienten zugeordnet werden können.
+Dies lässt sich durch weitere Pseudonyme abbilden, allerdings werden pro Originalwert mehrere
+Pseudonyme benötigt.
+Zu diesem Zweck muss in gPas eine **Multi-Pseudonym-Domäne** konfiguriert werden (siehe auch
+*APP_PSEUDONYMIZE_GPAS_CCDN*).
+
+**WICHTIG:** Deaktivierte Pseudonymisierung ist nur für Tests nutzbar. Vorgangsummern sind zufällig
+und werden anschließend verworfen.
+
+#### Test Betriebsbereitschaft
Um die voll Betriebsbereitschaft herzustellen, muss eine erfolgreiche Übertragung mit dem
Submission-Typ *Test* erfolgt sein. Über die Umgebungsvariable wird dieser Übertragungsmodus
aktiviert. Alle Datensätze mit erteilter Teilnahme am Modelvorhaben werden mit der Test-Kennung
@@ -98,20 +109,21 @@ vergleichbare IDs bereitzustellen.
#### Eingebaute Anonymisierung
Wurde keine oder die Verwendung der eingebauten Anonymisierung konfiguriert, so wird für die
-Patienten-ID der
-entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende "=" - zuzüglich des
-konfigurierten Präfixes
-als Patienten-Pseudonym verwendet.
+Patienten-ID der entsprechende SHA-256-Hash gebildet und Base64-codiert - hier ohne endende
+"=" - zuzüglich des konfigurierten Präfixes als Patienten-Pseudonym verwendet.
#### Pseudonymisierung mit gPAS
-Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfigurieren.
+Wurde die Verwendung von gPAS konfiguriert, so sind weitere Angaben zu konfigurieren.
+
+Ab Version 2025.1 (Multi-Pseudonym Support)
-* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz inklusive Endpoint (z.B.
- `http://localhost:8080/ttp-fhir/fhir/gpas/$$pseudonymizeAllowCreate`)
-* `APP_PSEUDONYMIZE_GPAS_TARGET`: gPas Domänenname
+* `APP_PSEUDONYMIZE_GPAS_URI`: URI der gPAS-Instanz REST API (e.g. http://127.0.0.1:9990/ttp-fhir/fhir/gpas)
* `APP_PSEUDONYMIZE_GPAS_USERNAME`: gPas Basic-Auth Benutzername
* `APP_PSEUDONYMIZE_GPAS_PASSWORD`: gPas Basic-Auth Passwort
+* `APP_PSEUDONYMIZE_GPAS_PID_DOMAIN`: gPas Domänenname für Patienten ID
+* `APP_PSEUDONYMIZE_GPAS_GENOM_DE_TAN_DOMAIN`: gPas Multi-Pseudonym-Domäne für genomDE Vorgangsnummern (
+ Clinical data node)
### (Externe) Consent-Services
@@ -173,8 +185,7 @@ Modelvorhaben §64e.
### Anmeldung mit einem Passwort
Ein initialer Administrator-Account kann optional konfiguriert werden und sorgt dafür, dass
-bestimmte Bereiche nur nach
-einem erfolgreichen Login erreichbar sind.
+bestimmte Bereiche nur nach einem erfolgreichen Login erreichbar sind.
* `APP_SECURITY_ADMIN_USER`: Muss angegeben werden zur Aktivierung der Zugriffsbeschränkung.
* `APP_SECURITY_ADMIN_PASSWORD`: Das Passwort für den Administrator (Empfohlen).
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
index 1275239..c2a8ba6 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
@@ -43,14 +43,14 @@ class GpasPseudonymGeneratorTest {
private lateinit var mockRestServiceServer: MockRestServiceServer
private lateinit var generator: GpasPseudonymGenerator
private lateinit var restTemplate: RestTemplate
- private var appFhirConfig: AppFhirConfig = AppFhirConfig()
+ private var appFhirConfig: AppFhirConfig = AppFhirConfig()
@BeforeEach
fun setup() {
val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
val gPasConfigProperties = GPasConfigProperties(
- "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
- "test",
+ "https://localhost:9990/ttp-fhir/fhir/gpas",
+ "test", "test2",
null,
null
)
diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
index e1938ba..8d0d0c1 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/Generator.java
@@ -23,4 +23,6 @@ public interface Generator {
String generate(String id);
+ String generateGenomDeTan(String id);
+
}
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 a22100b..6a2b947 100644
--- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java
@@ -23,7 +23,11 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GPasConfigProperties;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.core5.net.URIBuilder;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
@@ -42,38 +46,67 @@ public class GpasPseudonymGenerator implements Generator {
private final FhirContext r4Context;
private final String gPasUrl;
- private final String psnTargetDomain;
private final HttpHeaders httpHeader;
private final RetryTemplate retryTemplate;
private final Logger log = LoggerFactory.getLogger(GpasPseudonymGenerator.class);
-
private final RestTemplate restTemplate;
+ private final @NotNull String genomDeTanDomain;
+ private final @NotNull String pidPsnDomain;
+ protected final static String createOrGetPsn = "$pseudonymizeAllowCreate";
+ protected final static String createMultiDomainPsn = "$pseudonymize-secondary";
+ private final static String SINGLE_PSN_PART_NAME = "pseudonym";
+ private final static String MULTI_PSN_PART_NAME = "value";
public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate,
RestTemplate restTemplate, AppFhirConfig appFhirConfig) {
this.retryTemplate = retryTemplate;
this.restTemplate = restTemplate;
this.gPasUrl = gpasCfg.getUri();
- this.psnTargetDomain = gpasCfg.getTarget();
+ this.pidPsnDomain = gpasCfg.getPatientDomain();
+ this.genomDeTanDomain = gpasCfg.getGenomDeTanDomain();
this.r4Context = appFhirConfig.fhirContext();
httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword());
- log.debug(String.format("%s has been initialized", this.getClass().getName()));
+ log.debug("{} has been initialized", this.getClass().getName());
}
@Override
public String generate(String id) {
- var gPasRequestBody = getGpasRequestBody(id);
- var responseEntity = getGpasPseudonym(gPasRequestBody);
- var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
- .parseResource(responseEntity.getBody());
+ return generate(id, PsnDomainType.SINGLE_PSN_DOMAIN);
+ }
+
+ @Override
+ public String generateGenomDeTan(String id) {
+ return generate(id, PsnDomainType.MULTI_PSN_DOMAIN);
+ }
- return unwrapPseudonym(gPasPseudonymResult);
+ protected String generate(String id, PsnDomainType domainType) {
+ switch (domainType) {
+ case SINGLE_PSN_DOMAIN -> {
+ final var requestBody = createSinglePsnRequestBody(id, pidPsnDomain);
+ final var responseEntity = getGpasPseudonym(requestBody, createOrGetPsn);
+ final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
+ .parseResource(responseEntity.getBody());
+
+ return unwrapPseudonym(gPasPseudonymResult, SINGLE_PSN_PART_NAME);
+ }
+ case MULTI_PSN_DOMAIN -> {
+ final var requestBody = createMultiPsnRequestBody(id, genomDeTanDomain);
+ final var responseEntity = getGpasPseudonym(requestBody, createMultiDomainPsn);
+ final var gPasPseudonymResult = (Parameters) r4Context.newJsonParser()
+ .parseResource(responseEntity.getBody());
+
+ return unwrapPseudonym(gPasPseudonymResult, MULTI_PSN_PART_NAME);
+ }
+ }
+ throw new NotImplementedException(
+ "give domain type '%s' is unexpected and is currently not supported!".formatted(
+ domainType));
}
@NotNull
- public static String unwrapPseudonym(Parameters gPasPseudonymResult) {
+ public static String unwrapPseudonym(Parameters gPasPseudonymResult, String targetPartName) {
final var parameters = gPasPseudonymResult.getParameter().stream().findFirst();
if (parameters.isEmpty()) {
@@ -81,7 +114,7 @@ public class GpasPseudonymGenerator implements Generator {
}
final var identifier = (Identifier) parameters.get().getPart().stream()
- .filter(a -> a.getName().equals("pseudonym"))
+ .filter(a -> a.getName().equals(targetPartName))
.findFirst()
.orElseGet(ParametersParameterComponent::new).getValue();
@@ -104,13 +137,14 @@ public class GpasPseudonymGenerator implements Generator {
}
@NotNull
- protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody) {
+ protected ResponseEntity<String> getGpasPseudonym(String gPasRequestBody, String apiEndpoint) {
HttpEntity<String> requestEntity = new HttpEntity<>(gPasRequestBody, this.httpHeader);
try {
+ var targetUrl = buildRequestUrl(apiEndpoint);
ResponseEntity<String> responseEntity = retryTemplate.execute(
- ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity,
+ ctx -> restTemplate.exchange(targetUrl, HttpMethod.POST, requestEntity,
String.class));
if (responseEntity.getStatusCode().is2xxSuccessful()) {
log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode());
@@ -139,16 +173,43 @@ public class GpasPseudonymGenerator implements Generator {
}
- protected String getGpasRequestBody(String id) {
- var requestParameters = new Parameters();
+ protected URI buildRequestUrl(String apiEndpoint) throws URISyntaxException {
+ var gPasUrl1 = gPasUrl;
+ if (gPasUrl.lastIndexOf("/") == gPasUrl.length() - 1) {
+ gPasUrl1 = gPasUrl.substring(0, gPasUrl.length() - 1);
+ }
+ var urlBuilder = new URIBuilder(new URI(gPasUrl1)).appendPath(apiEndpoint);
+
+ return urlBuilder.build();
+ }
+
+ protected String createSinglePsnRequestBody(String id, String targetDomain) {
+ final var requestParameters = new Parameters();
requestParameters.addParameter().setName("target")
- .setValue(new StringType().setValue(psnTargetDomain));
+ .setValue(new StringType().setValue(targetDomain));
requestParameters.addParameter().setName("original")
.setValue(new StringType().setValue(id));
final IParser iParser = r4Context.newJsonParser();
return iParser.encodeResourceToString(requestParameters);
}
+ protected String createMultiPsnRequestBody(String id, String targetDomain) {
+ final var param = new Parameters();
+ ParametersParameterComponent targetParam = param.addParameter().setName("original");
+ targetParam.addPart(
+ new ParametersParameterComponent().setName("target")
+ .setValue(new StringType(targetDomain)));
+ targetParam.addPart(
+ new ParametersParameterComponent().setName("value").setValue(new StringType(id)));
+ targetParam
+ .addPart(new ParametersParameterComponent().setName("count").setValue(
+ new StringType("1")));
+
+ final IParser iParser = r4Context.newJsonParser();
+ return iParser.encodeResourceToString(param);
+ }
+
+
@NotNull
protected HttpHeaders getHttpHeaders(String gPasUserName, String gPasPassword) {
var headers = new HttpHeaders();
diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java
new file mode 100644
index 0000000..a0fbc93
--- /dev/null
+++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/PsnDomainType.java
@@ -0,0 +1,12 @@
+package dev.dnpm.etl.processor.pseudonym;
+
+public enum PsnDomainType {
+ /**
+ * one pseudonym per original value
+ */
+ SINGLE_PSN_DOMAIN,
+ /**
+ * multiple pseudonymes for one original value
+ */
+ MULTI_PSN_DOMAIN
+}
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 a2ea032..207785e 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt
@@ -48,7 +48,8 @@ data class PseudonymizeConfigProperties(
@ConfigurationProperties(GPasConfigProperties.NAME)
data class GPasConfigProperties(
val uri: String?,
- val target: String = "etl-processor",
+ val patientDomain: String = "etl-processor",
+ val genomDeTanDomain: String = "ccdn",
val username: String?,
val password: String?,
) {
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt
index c03db12..0537cbb 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt
@@ -21,9 +21,12 @@ package dev.dnpm.etl.processor.pseudonym
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
-
+import java.security.SecureRandom
class AnonymizingGenerator : Generator {
+ companion object fun getSecureRandom() : SecureRandom {
+ return SecureRandom()
+ }
override fun generate(id: String): String {
return Base32().encodeAsString(DigestUtils.sha256(id))
@@ -31,4 +34,14 @@ class AnonymizingGenerator : Generator {
.lowercase()
}
+ @OptIn(ExperimentalStdlibApi::class)
+ override fun generateGenomDeTan(id: String?): String {
+
+ val bytes = ByteArray(64 / 2)
+ getSecureRandom().nextBytes(bytes)
+
+ return bytes.joinToString("") { "%02x".format(it) }
+
+ }
+
} \ 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 e80f6ec..96225a9 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt
@@ -35,6 +35,10 @@ class PseudonymizeService(
}
}
+ fun genomDeTan(patientId: PatientId): String {
+ return generator.generateGenomDeTan(patientId.value)
+ }
+
fun prefix(): String {
return configProperties.prefix
}
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 77f3399..50d5b20 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt
@@ -349,3 +349,8 @@ fun Mtb.ensureMetaDataIsInitialized() {
this.metadata.modelProjectConsent.provisions.toMutableList()
}
}
+
+infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService)
+{
+ this.metadata.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id))
+} \ No newline at end of file
diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
index 3841641..11aff57 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt
@@ -38,7 +38,7 @@ class ConsentProcessor(
/**
* In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked.
*
- * Logik:
+ * Logic:
* * <c>true</c> IF consent check is disabled.
* * <c>true</c> IF broad consent (BC) has been given.
* * <c>true</c> BC has been asked AND declined but genomDe consent has been consented.
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 bb226c0..f2e8390 100644
--- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
+++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt
@@ -30,8 +30,11 @@ import dev.dnpm.etl.processor.monitoring.RequestStatus
import dev.dnpm.etl.processor.monitoring.RequestType
import dev.dnpm.etl.processor.output.*
import dev.dnpm.etl.processor.pseudonym.PseudonymizeService
+import dev.dnpm.etl.processor.pseudonym.addGenomDeTan
import dev.dnpm.etl.processor.pseudonym.anonymizeContentWith
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
+import dev.pcvolkmer.mv64e.mtb.ConsentProvision
+import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose
import dev.pcvolkmer.mv64e.mtb.Mtb
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
@@ -76,9 +79,12 @@ class RequestProcessor(
fun processMtbFile(mtbFile: Mtb, requestId: RequestId) {
val pid = PatientId(extractPatientIdentifier(mtbFile))
- val isConsentOk = consentProcessor != null &&
- consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
+ val isConsentOk =
+ consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
if (isConsentOk) {
+ if (isGenomDeConsented(mtbFile)) {
+ mtbFile addGenomDeTan pseudonymizeService
+ }
mtbFile pseudonymizeWith pseudonymizeService
mtbFile anonymizeContentWith pseudonymizeService
val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile))
@@ -93,6 +99,13 @@ class RequestProcessor(
}
}
+ private fun isGenomDeConsented(mtbFile: Mtb): Boolean {
+ val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p ->
+ p.purpose == ModelProjectConsentPurpose.SEQUENCING && p.type == ConsentProvision.PERMIT
+ } == true
+ return isModelProjectConsented
+ }
+
private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) {
requestService.save(
Request(
diff --git a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt
index 4646ff6..819454f 100644
--- a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt
+++ b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt
@@ -71,8 +71,8 @@ class PseudonymizeServiceTest {
}
@Test
- fun sanitizeFileName(@Mock generator: GpasPseudonymGenerator) {
- val result= GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
+ fun sanitizeFileName() {
+ val result = GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>")
assertThat(result).isEqualTo("l___a_bs_1_2_3_")
}
@@ -90,4 +90,16 @@ class PseudonymizeServiceTest {
assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123")
}
+ @Test
+ fun shouldReturnDifferentValues() {
+ val ag = AnonymizingGenerator()
+
+ val tans = HashSet<String>()
+
+ (1..1000).forEach { i ->
+ val tan = ag.generateGenomDeTan("12345")
+ assertThat(tan).hasSize(64)
+ assertThat(tans.add(tan)).`as`("never the same result!").isTrue
+ }
+ }
} \ No newline at end of file