/*
* This file is part of ETL-Processor
*
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken
* Copyright (c) 2025-2026 Paul-Christian Volkmer, 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 .
*/
package dev.dnpm.etl.processor.pseudonym
import ca.uhn.fhir.context.FhirContext
import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.config.GIcsConfigProperties
import dev.dnpm.etl.processor.config.JacksonConfig
import dev.dnpm.etl.processor.consent.MtbFileConsentService
import dev.dnpm.etl.processor.services.ConsentProcessor
import dev.dnpm.etl.processor.services.ConsentProcessorTest
import dev.pcvolkmer.mv64e.mtb.*
import org.assertj.core.api.Assertions.assertThat
import org.hl7.fhir.r4.model.Bundle
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.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.anyValueClass
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.whenever
import org.springframework.core.io.ClassPathResource
import tools.jackson.databind.json.JsonMapper
import java.time.Instant
import java.util.*
@ExtendWith(MockitoExtension::class)
class ExtensionsTest {
fun getJsonMapper(): JsonMapper {
return JacksonConfig().jsonMapper()
}
@Nested
inner class UsingDnpmV2Datamodel {
val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json"
val CLEAN_PATIENT_ID = "644bae7a-56f6-4ee8-b02f-c532e65af5b1"
private fun fakeMtbFile(): Mtb {
val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream
return getJsonMapper().readValue(mtbFile, Mtb::class.java)
}
private fun Mtb.serialized(): String {
return getJsonMapper().writeValueAsString(this)
}
@Test
fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
}
.whenever(pseudonymizeService)
.patientPseudonym(anyValueClass())
val mtbFile = fakeMtbFile()
mtbFile.ensureMetaDataIsInitialized()
addConsentData(mtbFile)
mtbFile.pseudonymizeWith(pseudonymizeService)
assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID")
assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID)
}
private fun addConsentData(mtbFile: Mtb) {
val gIcsConfigProperties = GIcsConfigProperties("", "", "")
val appConfigProperties = AppConfigProperties(emptyList())
val bundle = Bundle()
val dummyConsent = ConsentProcessorTest.getDummyGenomDeConsent()
dummyConsent.patient.reference = "Patient/$CLEAN_PATIENT_ID"
bundle.addEntry().resource = dummyConsent
ConsentProcessor(
appConfigProperties,
gIcsConfigProperties,
JacksonConfig().jsonMapper(),
FhirContext.forR4(),
MtbFileConsentService(),
)
.embedBroadConsentResources(mtbFile, bundle)
}
@Test
fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
}
.whenever(pseudonymizeService)
.patientPseudonym(anyValueClass())
doAnswer { "TESTDOMAIN" }.whenever(pseudonymizeService).prefix()
val mtbFile =
Mtb().apply {
this.patient =
Patient().apply {
this.id = "PID"
this.birthDate = Date.from(Instant.now())
this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE }
}
this.episodesOfCare =
listOf(
MtbEpisodeOfCare().apply {
this.id = "1"
this.patient = Reference().apply { this.id = "PID" }
this.period = PeriodDate().apply { this.start = Date.from(Instant.now()) }
}
)
}
mtbFile.pseudonymizeWith(pseudonymizeService)
mtbFile.anonymizeContentWith(pseudonymizeService)
assertThat(mtbFile.episodesOfCare).hasSize(1)
assertThat(mtbFile.episodesOfCare.map { it.id }).isNotNull
}
@Test
fun shouldNotContainAnyUuidAfterRehashingOfIds(@Mock pseudonymizeService: PseudonymizeService) {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
}
.whenever(pseudonymizeService)
.patientPseudonym(anyValueClass())
doAnswer { "TESTDOMAIN" }.whenever(pseudonymizeService).prefix()
val mtbFile = fakeMtbFile()
/** replace hex values with random long, so our test does not match false positives */
mtbFile.ngsReports.forEach { report ->
report.results.simpleVariants.forEach { simpleVariant ->
simpleVariant.externalIds.forEach { extIdValue ->
extIdValue.value = Math.random().toLong().toString()
}
}
}
mtbFile.ngsReports.forEach { report ->
report.results.rnaFusions.forEach { simpleVariant ->
simpleVariant.externalIds.forEach { extIdValue ->
extIdValue.value = Math.random().toLong().toString()
}
simpleVariant.fusionPartner3Prime?.transcriptId?.value = Math.random().toLong().toString()
simpleVariant.fusionPartner5Prime?.transcriptId?.value = Math.random().toLong().toString()
simpleVariant.externalIds?.forEach { it?.value = Math.random().toLong().toString() }
}
}
mtbFile.pseudonymizeWith(pseudonymizeService)
mtbFile.anonymizeContentWith(pseudonymizeService)
val pattern =
"\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex().toPattern()
val input = mtbFile.serialized()
val matcher = pattern.matcher(input)
assertThrows {
matcher.find()
val posSt = "check at pos: " + matcher.start().toString() + ", " + matcher.end()
println(posSt + " with " + matcher.group())
}
.also { assertThat(it.message).isEqualTo("No match found") }
}
}
@Test
fun shouldUseSameAnonymIdForDiagnosisAndDiagnosisReferences(
@Mock pseudonymizeService: PseudonymizeService
) {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
}
.whenever(pseudonymizeService)
.patientPseudonym(anyValueClass())
doAnswer { "TESTDOMAIN" }.whenever(pseudonymizeService).prefix()
val mtbFile =
Mtb().apply {
this.patient =
Patient().apply {
this.id = "PID"
this.birthDate = Date.from(Instant.now())
this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE }
}
this.diagnoses = listOf(MtbDiagnosis().apply { this.id = "Diagnosis-1" })
this.episodesOfCare =
listOf(
MtbEpisodeOfCare().apply {
this.id = "Episode-1"
this.diagnoses = listOf(Reference().apply { this.id = "Diagnosis-1" })
}
)
this.guidelineTherapies =
listOf(
MtbSystemicTherapy().apply {
this.id = "Systemic-Therapy-1"
this.reason = Reference().apply { this.id = "Diagnosis-1" }
}
)
this.guidelineProcedures =
listOf(
OncoProcedure().apply {
this.id = "Onco-Procedure-1"
this.reason = Reference().apply { this.id = "Diagnosis-1" }
}
)
this.specimens =
listOf(
TumorSpecimen().apply {
this.id = "Specimen-1"
this.diagnosis = Reference().apply { this.id = "Diagnosis-1" }
}
)
}
mtbFile.pseudonymizeWith(pseudonymizeService)
mtbFile.anonymizeContentWith(pseudonymizeService)
assertThat(mtbFile.diagnoses.first().id)
.isEqualTo(mtbFile.episodesOfCare.first().diagnoses.first().id)
assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.guidelineTherapies.first().reason.id)
assertThat(mtbFile.diagnoses.first().id)
.isEqualTo(mtbFile.guidelineProcedures.first().reason.id)
assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.specimens.first().diagnosis.id)
}
@Test
fun shouldNotThrowAnyExceptionOnMissingMsiId(@Mock pseudonymizeService: PseudonymizeService) {
doAnswer {
it.arguments[0]
"PSEUDO-ID"
}
.whenever(pseudonymizeService)
.patientPseudonym(anyValueClass())
doAnswer { "TESTDOMAIN" }.whenever(pseudonymizeService).prefix()
val mtbFile =
Mtb().apply {
this.patient =
Patient().apply {
this.id = "PID"
this.birthDate = Date.from(Instant.now())
this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE }
}
this.msiFindings =
listOf(
null,
Msi.builder().id("1").build(),
Msi.builder().build(),
Msi.builder().specimen(null).build(),
Msi.builder().specimen(Reference.builder().build()).build(),
)
}
mtbFile.pseudonymizeWith(pseudonymizeService)
mtbFile.anonymizeContentWith(pseudonymizeService)
assertThat(mtbFile.msiFindings).isNotNull
assertThat(mtbFile.msiFindings[1])
.satisfiesAnyOf(
{ assertThat(it.id).isNull() },
{ assertThat(it.id).isEqualTo("TESTDOMAIN44e20a53bbbf9f3ae39626d05df7014dcd77d6098") },
)
}
}