1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-16 20:32:54 +00:00

refactor: moved consent evaluation to new ConsentProcessor.kt

This commit is contained in:
Jakub Lidke
2025-07-16 15:20:26 +02:00
parent 38eec60189
commit dc6a4c6cb9
13 changed files with 310 additions and 277 deletions

View File

@ -23,9 +23,9 @@ 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.consent.ConsentCheckFileBased
import dev.dnpm.etl.processor.consent.ConsentByMtbFile
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.IGetConsent
import dev.dnpm.etl.processor.security.TokenRepository
import dev.dnpm.etl.processor.security.UserRoleRepository
import dev.dnpm.etl.processor.services.RequestProcessor
@ -55,7 +55,7 @@ import org.springframework.test.web.servlet.post
classes = [
MtbFileRestController::class,
AppSecurityConfiguration::class,
ConsentCheckFileBased::class, ICheckConsent::class
ConsentByMtbFile::class, IGetConsent::class
]
)
@MockitoBean(types = [TokenRepository::class, RequestProcessor::class])

View File

@ -1,18 +1,15 @@
package dev.dnpm.etl.processor.consent;
import dev.pcvolkmer.mv64e.mtb.Mtb;
import java.util.Date;
import org.apache.commons.lang3.NotImplementedException;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConsentCheckFileBased implements ICheckConsent {
public class ConsentByMtbFile implements IGetConsent {
private static final Logger log = LoggerFactory.getLogger(ConsentCheckFileBased.class);
private static final Logger log = LoggerFactory.getLogger(ConsentByMtbFile.class);
public ConsentCheckFileBased() {
public ConsentByMtbFile() {
log.info("ConsentCheckFileBased initialized...");
}
@ -23,12 +20,12 @@ public class ConsentCheckFileBased implements ICheckConsent {
@Override
public Bundle getBroadConsent(String personIdentifierValue, Date requestDate) {
return ICheckConsent.super.getBroadConsent(personIdentifierValue, requestDate);
return IGetConsent.super.getBroadConsent(personIdentifierValue, requestDate);
}
@Override
public Bundle getGenomDeConsent(String personIdentifierValue, Date requestDate) {
return ICheckConsent.super.getGenomDeConsent(personIdentifierValue, requestDate);
return IGetConsent.super.getGenomDeConsent(personIdentifierValue, requestDate);
}
@Override
@ -36,10 +33,4 @@ public class ConsentCheckFileBased implements ICheckConsent {
ConsentDomain targetConsentDomain, Date requestDate) {
return new Bundle();
}
@Override
public ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle,
Date requestDate, ConsentDomain consentDomain) {
return ConsentProvisionType.NULL;
}
}

View File

@ -2,27 +2,18 @@ package dev.dnpm.etl.processor.consent;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.DataFormatException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GIcsConfigProperties;
import java.util.Date;
import java.util.Optional;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
import org.hl7.fhir.r4.model.Consent.ConsentState;
import org.hl7.fhir.r4.model.Consent.ProvisionComponent;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.Period;
import org.hl7.fhir.r4.model.ResourceType;
import org.hl7.fhir.r4.model.StringType;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@ -39,7 +30,7 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
public class GicsConsentService implements ICheckConsent {
public class GicsConsentService implements IGetConsent {
private final Logger log = LoggerFactory.getLogger(GicsConsentService.class);
@ -272,83 +263,4 @@ public class GicsConsentService implements ICheckConsent {
return TtpConsentStatus.FAILED_TO_ASK;
}
/**
* @param consentBundle consent resource
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be {@link ConsentProvisionType#NULL} if none is found.
*/
public ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle,
Date requestDate, ConsentDomain consentDomain) {
String code;
String system;
if (ConsentDomain.BroadConsent == consentDomain) {
code = gIcsConfigProperties.getBroadConsentPolicyCode();
system = gIcsConfigProperties.getBroadConsentPolicySystem();
} else if (ConsentDomain.Modelvorhaben64e == consentDomain) {
code = gIcsConfigProperties.getGenomeDePolicyCode();
system = gIcsConfigProperties.getGenomeDePolicySystem();
} else {
throw new NotImplementedException("unknown consent domain " + consentDomain.name());
}
Optional<ConsentProvisionType> provisionTypeByPolicyCode = getProvisionTypeByPolicyCode(
consentBundle, code,
system, requestDate);
return provisionTypeByPolicyCode.orElse(ConsentProvisionType.NULL);
}
/**
* @param consentBundle consent resource
* @param policyAndProvisionCode policyRule and provision code value
* @param policyAndProvisionSystem policyRule and provision system value
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be {@link ConsentProvisionType#NULL} if none is found.
*/
public Optional<ConsentProvisionType> getProvisionTypeByPolicyCode(Bundle consentBundle,
String policyAndProvisionCode, String policyAndProvisionSystem, Date requestDate) {
return consentBundle.getEntry().stream().filter(entry -> {
if (entry.getResource().getResourceType() != ResourceType.Consent) {
// no consent in bundle
return false;
}
Consent consent = (Consent) entry.getResource();
// consent ist active and its policy rule must fits search criteria
return consent.getStatus() == ConsentState.ACTIVE && checkCoding(
policyAndProvisionCode, policyAndProvisionSystem,
consent.getPolicyRule().getCodingFirstRep()) && isIsRequestDateInRange(requestDate,
consent.getProvision().getPeriod());
}).map(consentWithTargetPolicy -> {
ProvisionComponent provision = ((Consent) consentWithTargetPolicy.getResource()).getProvision();
var provisionComponentByCode = provision.getProvision().stream().filter(prov ->
checkCoding(policyAndProvisionCode, policyAndProvisionSystem,
prov.getCodeFirstRep().getCodingFirstRep()) && isIsRequestDateInRange(
requestDate, prov.getPeriod())
).findFirst();
if (provisionComponentByCode.isPresent()) {
// actual provision we search for
return provisionComponentByCode.get().getType();
}
// no fitting nested provision found - fall back to wrapping provision with default value
return provision.getType();
}).findFirst().or(() -> Optional.of(ConsentProvisionType.NULL));
}
protected static boolean checkCoding(String researchAllowedPolicyOid,
String researchAllowedPolicySystem, Coding coding) {
return coding.getSystem().equals(researchAllowedPolicySystem) && coding.getCode()
.equals(researchAllowedPolicyOid);
}
protected static boolean isIsRequestDateInRange(Date requestdate, Period provPeriod) {
var isRequestDateAfterOrEqualStart = provPeriod.getStart().compareTo(requestdate);
var isRequestDateBeforeOrEqualEnd = provPeriod.getEnd().compareTo(requestdate);
return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0;
}
}

View File

@ -2,9 +2,8 @@ package dev.dnpm.etl.processor.consent;
import java.util.Date;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
public interface ICheckConsent {
public interface IGetConsent {
/**
* Get broad consent status for a patient identifier
@ -50,12 +49,4 @@ public interface ICheckConsent {
Bundle currentConsentForPersonAndTemplate(String personIdentifierValue,
ConsentDomain targetConsentDomain, Date requestDate);
/**
* @param consentBundle consent resource
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be {@link ConsentProvisionType#NULL} if none is found.
*/
ConsentProvisionType getProvisionTypeByPolicyCode(Bundle consentBundle, Date requestDate,
ConsentDomain consentDomain);
}

View File

@ -20,8 +20,8 @@
package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.consent.ConsentCheckFileBased
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.ConsentByMtbFile
import dev.dnpm.etl.processor.consent.IGetConsent
import dev.dnpm.etl.processor.consent.GicsConsentService
import dev.dnpm.etl.processor.monitoring.*
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
@ -212,7 +212,7 @@ class AppConfiguration {
fun gicsConsentService(
gIcsConfigProperties: GIcsConfigProperties,
retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig
): ICheckConsent {
): IGetConsent {
return GicsConsentService(
gIcsConfigProperties,
retryTemplate,
@ -227,7 +227,7 @@ class AppConfiguration {
gIcsConfigProperties: GIcsConfigProperties,
getObjectMapper: ObjectMapper,
appFhirConfig: AppFhirConfig,
gicsConsentService: ICheckConsent
gicsConsentService: IGetConsent
): ConsentProcessor {
return ConsentProcessor(
gIcsConfigProperties,
@ -253,8 +253,8 @@ class AppConfiguration {
@Bean
@ConditionalOnMissingBean
fun constService(): ICheckConsent {
return ConsentCheckFileBased()
fun iGetConsentService(): IGetConsent {
return ConsentByMtbFile()
}
}

View File

@ -23,7 +23,7 @@ import de.ukw.ccc.bwhc.dto.Consent
import de.ukw.ccc.bwhc.dto.MtbFile
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.PatientId
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.IGetConsent
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.Mtb
@ -35,7 +35,7 @@ import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping(path = ["mtbfile", "mtb"])
class MtbFileRestController(
private val requestProcessor: RequestProcessor, private val iCheckConsent: ICheckConsent
private val requestProcessor: RequestProcessor, private val iGetConsent: IGetConsent
) {
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
@ -63,7 +63,7 @@ class MtbFileRestController(
}
private fun checkConsentStatus(mtbFile: MtbFile): Pair<TtpConsentStatus, Boolean> {
var ttpConsentStatus = iCheckConsent.getTtpBroadConsentStatus(mtbFile.patient.id)
var ttpConsentStatus = iGetConsent.getTtpBroadConsentStatus(mtbFile.patient.id)
val isConsentOK =
(ttpConsentStatus.equals(TtpConsentStatus.UNKNOWN_CHECK_FILE) && mtbFile.consent.status == Consent.Status.ACTIVE) ||

View File

@ -6,12 +6,14 @@ import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.config.GIcsConfigProperties
import dev.dnpm.etl.processor.consent.ConsentDomain
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.IGetConsent
import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized
import dev.pcvolkmer.mv64e.mtb.*
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Consent
import org.apache.commons.lang3.NotImplementedException
import org.hl7.fhir.r4.model.*
import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Consent.ConsentState
import org.hl7.fhir.r4.model.Consent.ProvisionComponent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -23,9 +25,10 @@ import java.util.*
@Service
class ConsentProcessor(
private val gIcsConfigProperties: GIcsConfigProperties, private val objectMapper: ObjectMapper,
private val gIcsConfigProperties: GIcsConfigProperties,
private val objectMapper: ObjectMapper,
private val fhirContext: FhirContext,
private val consentService: ICheckConsent?
private val consentService: IGetConsent?
) {
private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor")
@ -80,11 +83,11 @@ class ConsentProcessor(
embedBroadConsentResources(mtbFile, broadConsent)
val broadConsentStatus = consentService.getProvisionTypeByPolicyCode(
val broadConsentStatus = getProvisionTypeByPolicyCode(
broadConsent, requestDate, ConsentDomain.BroadConsent
)
val genomDeSequencingStatus = consentService.getProvisionTypeByPolicyCode(
val genomDeSequencingStatus = getProvisionTypeByPolicyCode(
genomeDeConsent, requestDate, ConsentDomain.Modelvorhaben64e
)
@ -92,27 +95,22 @@ class ConsentProcessor(
// bc not asked
return false
}
if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus ||
Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus
) return true
if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus || Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus) return true
return false
}
public fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) {
fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) {
for (entry in broadConsent.getEntry()) {
val resource = entry.getResource()
if (resource is Consent) {
// since jackson convertValue does not work here,
// we need another step to back to string, before we convert to object map
val asJsonString =
fhirContext.newJsonParser().encodeResourceToString(resource)
val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource)
try {
val mapOfJson: HashMap<String?, Any?>? =
objectMapper.readValue<HashMap<String?, Any?>?>(
asJsonString,
object : TypeReference<HashMap<String?, Any?>?>() {
})
asJsonString, object : TypeReference<HashMap<String?, Any?>?>() {})
mtbFile.metadata.researchConsents.add(mapOfJson)
} catch (e: JsonProcessingException) {
throw RuntimeException(e)
@ -121,7 +119,7 @@ class ConsentProcessor(
}
}
public fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) {
fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) {
for (entry in consentGnomeDe.getEntry()) {
val resource = entry.getResource()
if (resource !is Consent) {
@ -157,8 +155,7 @@ class ConsentProcessor(
val provision = Provision.builder()
.type(ConsentProvision.valueOf(provisionComponent.getType().name))
.date(provisionComponent.getPeriod().getStart())
.purpose(modelProjectConsentPurpose)
.build()
.purpose(modelProjectConsentPurpose).build()
mtbFile.metadata.modelProjectConsent.provisions.add(provision)
} catch (ioe: IOException) {
@ -183,4 +180,89 @@ class ConsentProcessor(
mtbFile.metadata.type = MvhSubmissionType.INITIAL
}
/**
* @param consentBundle consent resource
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found.
*/
fun getProvisionTypeByPolicyCode(
consentBundle: Bundle, requestDate: Date?, consentDomain: ConsentDomain
): Consent.ConsentProvisionType {
val code: String?
val system: String?
if (ConsentDomain.BroadConsent == consentDomain) {
code = gIcsConfigProperties.broadConsentPolicyCode
system = gIcsConfigProperties.broadConsentPolicySystem
} else if (ConsentDomain.Modelvorhaben64e == consentDomain) {
code = gIcsConfigProperties.genomeDePolicyCode
system = gIcsConfigProperties.genomeDePolicySystem
} else {
throw NotImplementedException("unknown consent domain " + consentDomain.name)
}
val provisionTypeByPolicyCode = getProvisionTypeByPolicyCode(
consentBundle, code, system, requestDate
)
return provisionTypeByPolicyCode
}
/**
* @param consentBundle consent resource
* @param policyAndProvisionCode policyRule and provision code value
* @param policyAndProvisionSystem policyRule and provision system value
* @param requestDate date which must be within validation period of provision
* @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found.
*/
fun getProvisionTypeByPolicyCode(
consentBundle: Bundle,
policyAndProvisionCode: String?,
policyAndProvisionSystem: String?,
requestDate: Date?
): Consent.ConsentProvisionType {
val entriesOfInterest = consentBundle.entry.filter { entry ->
entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent && (entry.resource as Consent).status == ConsentState.ACTIVE && checkCoding(
policyAndProvisionCode,
policyAndProvisionSystem,
(entry.resource as Consent).policyRule.codingFirstRep
) && isIsRequestDateInRange(
requestDate, (entry.resource as Consent).provision.period
)
}.map { consentWithTargetPolicy: BundleEntryComponent ->
val provision = (consentWithTargetPolicy.getResource() as Consent).getProvision()
val provisionComponentByCode =
provision.getProvision().stream().filter { prov: ProvisionComponent? ->
checkCoding(
policyAndProvisionCode,
policyAndProvisionSystem,
prov!!.getCodeFirstRep().getCodingFirstRep()
) && isIsRequestDateInRange(
requestDate, prov.getPeriod()
)
}.findFirst()
if (provisionComponentByCode.isPresent) {
// actual provision we search for
return@map provisionComponentByCode.get().getType()
} else {
if (provision.type != null) return provision.type
}
return Consent.ConsentProvisionType.NULL
}.firstOrNull()
if (entriesOfInterest == null) return Consent.ConsentProvisionType.NULL
return entriesOfInterest
}
fun checkCoding(
researchAllowedPolicyOid: String?, researchAllowedPolicySystem: String?, coding: Coding
): Boolean {
return coding.getSystem() == researchAllowedPolicySystem && (coding.getCode() == researchAllowedPolicyOid)
}
fun isIsRequestDateInRange(requestdate: Date?, provPeriod: Period): Boolean {
val isRequestDateAfterOrEqualStart = provPeriod.getStart().compareTo(requestdate)
val isRequestDateBeforeOrEqualEnd = provPeriod.getEnd().compareTo(requestdate)
return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0
}
}

View File

@ -23,8 +23,6 @@ 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.consent.ConsentDomain
import dev.dnpm.etl.processor.consent.ICheckConsent
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.monitoring.Report
import dev.dnpm.etl.processor.monitoring.Request
@ -33,19 +31,15 @@ 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.anonymizeContentWith
import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized
import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith
import dev.pcvolkmer.mv64e.mtb.Mtb
import dev.pcvolkmer.mv64e.mtb.MvhSubmissionType
import org.apache.commons.codec.binary.Base32
import org.apache.commons.codec.digest.DigestUtils
import org.hl7.fhir.r4.model.Consent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import java.lang.RuntimeException
import java.time.Clock
import java.time.Instant
import java.util.*
@ -83,7 +77,7 @@ class RequestProcessor(
val pid = PatientId(extractPatientIdentifier(mtbFile))
val isConsentOk = consentProcessor != null &&
consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null;
consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null
if (isConsentOk) {
mtbFile pseudonymizeWith pseudonymizeService
mtbFile anonymizeContentWith pseudonymizeService
@ -190,7 +184,7 @@ class RequestProcessor(
)
)
} catch (e: Exception) {
} catch (_: Exception) {
requestService.save(
Request(
uuid = requestId,

View File

@ -3,20 +3,13 @@ package dev.dnpm.etl.processor.consent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import ca.uhn.fhir.context.FhirContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.dnpm.etl.processor.config.AppConfiguration;
import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GIcsConfigProperties;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.Date;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Consent.ConsentProvisionType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
@ -27,17 +20,13 @@ import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.client.MockRestServiceServer;
@ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class})
@TestPropertySource(properties = {"app.consent.gics.enabled=true",
"app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"})
@ -63,7 +52,6 @@ public class GicsConsentServiceTest {
@BeforeEach
public void setUp() {
mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate());
}
@Test
@ -72,12 +60,10 @@ public class GicsConsentServiceTest {
new ParametersParameterComponent().setName("consented")
.setValue(new BooleanType().setValue(true)));
mockRestServiceServer.expect(
requestTo("http://localhost:8090/ttp-fhir/fhir/gics"
+ GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
withSuccess(appFhirConfig.fhirContext().newJsonParser()
.encodeResourceToString(responseConsented),
MediaType.APPLICATION_JSON));
mockRestServiceServer.expect(requestTo(
"http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT))
.andRespond(withSuccess(appFhirConfig.fhirContext().newJsonParser()
.encodeResourceToString(responseConsented), MediaType.APPLICATION_JSON));
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN);
@ -89,11 +75,10 @@ public class GicsConsentServiceTest {
new ParametersParameterComponent().setName("consented")
.setValue(new BooleanType().setValue(false)));
mockRestServiceServer.expect(
requestTo("http://localhost:8090/ttp-fhir/fhir/gics"
+ GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
withSuccess(appFhirConfig.fhirContext().newJsonParser()
.encodeResourceToString(responseRevoced),
mockRestServiceServer.expect(requestTo(
"http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT))
.andRespond(withSuccess(
appFhirConfig.fhirContext().newJsonParser().encodeResourceToString(responseRevoced),
MediaType.APPLICATION_JSON));
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
@ -104,15 +89,13 @@ public class GicsConsentServiceTest {
@Test
void gicsParameterInvalid() {
final OperationOutcome responseErrorOutcome = new OperationOutcome().addIssue(
new OperationOutcomeIssueComponent().setSeverity(
IssueSeverity.ERROR).setCode(IssueType.PROCESSING)
.setDiagnostics("Invalid policy parameter..."));
new OperationOutcomeIssueComponent().setSeverity(IssueSeverity.ERROR)
.setCode(IssueType.PROCESSING).setDiagnostics("Invalid policy parameter..."));
mockRestServiceServer.expect(
requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond(
withSuccess(appFhirConfig.fhirContext().newJsonParser()
.encodeResourceToString(responseErrorOutcome),
MediaType.APPLICATION_JSON));
.encodeResourceToString(responseErrorOutcome), MediaType.APPLICATION_JSON));
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
@ -122,51 +105,19 @@ public class GicsConsentServiceTest {
void buildRequestParameterCurrentPolicyStatesForPersonTest() {
String pid = "12345678";
var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(gIcsConfigProperties,
pid, Date.from(Instant.now()),gIcsConfigProperties.getGenomDeConsentDomainName());
var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson(
gIcsConfigProperties, pid, Date.from(Instant.now()),
gIcsConfigProperties.getGenomDeConsentDomainName());
assertThat(result.getParameter().size()).as("should contain 3 parameter resources").isEqualTo(3);
assertThat(result.getParameter().size()).as("should contain 3 parameter resources")
.isEqualTo(3);
assertThat(((StringType)result.getParameter("domain").getValue()).getValue()).isEqualTo(gIcsConfigProperties.getGenomDeConsentDomainName());
assertThat(((Identifier)result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo(pid);
assertThat(((StringType) result.getParameter("domain").getValue()).getValue()).isEqualTo(
gIcsConfigProperties.getGenomDeConsentDomainName());
assertThat(
((Identifier) result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo(
pid);
}
@ParameterizedTest
@CsvSource({
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,PERMIT,expect permit",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on start",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2055-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on end",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2021-06-23T00:00:00+02:00,NULL,date is before start",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2060-06-23T00:00:00+02:00,NULL,date is after end",
"2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-07-23T00:00:00+02:00,NULL,system not found - therefore expect NULL",
"2.16.840.1.113883.3.1937.777.24.5.3.27,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,DENY,provision is denied"})
void getProvisionTypeByPolicyCode(String code, String system, String timeStamp, String expected,
String desc) {
var testData = getDummyBroadConsent();
Date requestDate = Date.from(OffsetDateTime.parse(timeStamp).toInstant());
var result = gicsConsentService.getProvisionTypeByPolicyCode(testData, code, system, requestDate);
assertThat(result).isNotNull();
assertThat(result).isNotEmpty();
assertThat(result.get()).as(desc).isEqualTo(ConsentProvisionType.valueOf(expected));
}
private Bundle getDummyBroadConsent() {
InputStream bundle;
try {
bundle = new ClassPathResource(
"fake_broadConsent_gics_response_permit.json").getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
return FhirContext.forR4().newJsonParser().parseResource(Bundle.class, bundle);
}
}

View File

@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
import de.ukw.ccc.bwhc.dto.Consent.Status
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.consent.ConsentCheckFileBased
import dev.dnpm.etl.processor.consent.ConsentByMtbFile
import dev.dnpm.etl.processor.consent.GicsConsentService
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor
@ -68,7 +68,7 @@ class MtbFileRestControllerTest {
) {
this.requestProcessor = requestProcessor
val controller = MtbFileRestController(requestProcessor,
ConsentCheckFileBased()
ConsentByMtbFile()
)
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
}
@ -220,7 +220,7 @@ class MtbFileRestControllerTest {
) {
this.requestProcessor = requestProcessor
val controller = MtbFileRestController(requestProcessor,
ConsentCheckFileBased()
ConsentByMtbFile()
)
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
}

View File

@ -26,7 +26,7 @@ import de.ukw.ccc.bwhc.dto.Patient
import dev.dnpm.etl.processor.config.GIcsConfigProperties
import dev.dnpm.etl.processor.config.JacksonConfig
import dev.dnpm.etl.processor.services.ConsentProcessor
import dev.dnpm.etl.processor.services.TransformationServiceTest
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
@ -252,7 +252,7 @@ class ExtensionsTest {
val bundle = Bundle()
val dummyConsent = TransformationServiceTest.getDummyConsent()
val dummyConsent = ConsentProcessorTest.getDummyGenomDeConsent()
dummyConsent.patient.reference = "Patient/$CLEAN_PATIENT_ID"
bundle.addEntry().resource = dummyConsent

View File

@ -0,0 +1,160 @@
package dev.dnpm.etl.processor.services
import ca.uhn.fhir.context.FhirContext
import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.config.GIcsConfigProperties
import dev.dnpm.etl.processor.config.JacksonConfig
import dev.dnpm.etl.processor.consent.GicsConsentService
import dev.pcvolkmer.mv64e.mtb.*
import org.assertj.core.api.Assertions.assertThat
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Consent
import org.junit.jupiter.api.BeforeEach
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.CsvSource
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.whenever
import org.springframework.core.io.ClassPathResource
import java.io.IOException
import java.io.InputStream
import java.time.Instant
import java.time.OffsetDateTime
import java.util.*
@ExtendWith(MockitoExtension::class)
class ConsentProcessorTest {
private lateinit var gicsConsentService: GicsConsentService
private lateinit var objectMapper: ObjectMapper
private lateinit var gIcsConfigProperties: GIcsConfigProperties
private lateinit var fhirContext: FhirContext
private lateinit var consentProcessor: ConsentProcessor
@BeforeEach
fun setups(
@Mock gicsConsentService: GicsConsentService,
) {
this.gIcsConfigProperties = GIcsConfigProperties(null, null, null, true)
val jacksonConfig = JacksonConfig()
this.objectMapper = jacksonConfig.objectMapper()
this.fhirContext = JacksonConfig.fhirContext()
this.gicsConsentService = gicsConsentService
this.consentProcessor =
ConsentProcessor(gIcsConfigProperties, objectMapper, fhirContext, gicsConsentService)
}
@Test
fun consentOk() {
assertThat(consentProcessor.toString()).isNotNull
// prep gICS response
doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService)
.getBroadConsent(any(), any())
doAnswer { Bundle() }.whenever(gicsConsentService)
.getGenomDeConsent(any(), any())
val inputMtb = Mtb.builder()
.patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build()
val checkResult = consentProcessor.consentGatedCheckAndTryEmbedding(inputMtb)
assertThat(checkResult).isTrue
assertThat(inputMtb.metadata.researchConsents).hasSize(13)
}
companion object {
fun getDummyGenomDeConsent(): Consent {
val consent = Consent()
consent.id = "consent 1 id"
consent.patient.reference = "Patient/1234-pat1"
consent.provision.setType(
Consent.ConsentProvisionType.fromCode(
"deny"
)
)
consent.provision.period.start =
Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
consent.provision.period.end =
Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
val addProvision1 = consent.provision.addProvision()
addProvision1.setType(Consent.ConsentProvisionType.fromCode("permit"))
addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
addProvision1.code.addLast(
CodeableConcept(
Coding(
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
"Teilnahme",
"Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
)
)
)
val addProvision2 = consent.provision.addProvision()
addProvision2.setType(Consent.ConsentProvisionType.fromCode("deny"))
addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
addProvision2.code.addLast(
CodeableConcept(
Coding(
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
"Rekontaktierung",
"Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
)
)
)
return consent
}
}
@ParameterizedTest
@CsvSource(
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,PERMIT,expect permit",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on start",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2055-06-23T00:00:00+02:00,PERMIT,expect permit date is exactly on end",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2021-06-23T00:00:00+02:00,NULL,date is before start",
"2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2060-06-23T00:00:00+02:00,NULL,date is after end",
"2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-07-23T00:00:00+02:00,NULL,system not found - therefore expect NULL",
"2.16.840.1.113883.3.1937.777.24.5.3.27,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-07-23T00:00:00+02:00,DENY,provision is denied"
)
fun getProvisionTypeByPolicyCode(
code: String?, system: String?, timeStamp: String, expected: String?,
desc: String?
) {
val testData = getDummyBroadConsentBundle()
val requestDate = Date.from(OffsetDateTime.parse(timeStamp).toInstant())
val result: Consent.ConsentProvisionType =
consentProcessor.getProvisionTypeByPolicyCode(testData, code, system, requestDate)
assertThat(result).isNotNull()
assertThat(result).`as`(desc)
.isEqualTo(Consent.ConsentProvisionType.valueOf(expected!!))
}
fun getDummyBroadConsentBundle(): Bundle {
val bundle: InputStream?
try {
bundle = ClassPathResource(
"fake_broadConsent_gics_response_permit.json"
).getInputStream()
} catch (e: IOException) {
throw RuntimeException(e)
}
return FhirContext.forR4().newJsonParser()
.parseResource<Bundle>(Bundle::class.java, bundle)
}
}

View File

@ -34,8 +34,6 @@ import dev.pcvolkmer.mv64e.mtb.Mtb
import dev.pcvolkmer.mv64e.mtb.MvhMetadata
import dev.pcvolkmer.mv64e.mtb.Provision
import org.hl7.fhir.instance.model.api.IBaseResource
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
import java.time.Instant
import java.util.Date
@ -145,7 +143,7 @@ class TransformationServiceTest {
.date(Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))).build()
)
).build()
val consent = getDummyConsent()
val consent = ConsentProcessorTest.getDummyGenomDeConsent()
mvhMetadata.researchConsents = mutableListOf()
mvhMetadata.researchConsents.add(mapOf(consent.id to consent as IBaseResource))
@ -157,51 +155,5 @@ class TransformationServiceTest {
}
companion object {
fun getDummyConsent(): org.hl7.fhir.r4.model.Consent {
val modelVorhabenConsent = org.hl7.fhir.r4.model.Consent()
modelVorhabenConsent.id = "consent 1 id"
modelVorhabenConsent.patient.reference = "Patient/1234-pat1"
modelVorhabenConsent.provision.setType(
org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode(
"deny"
)
)
modelVorhabenConsent.provision.period.start =
Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
modelVorhabenConsent.provision.period.end =
Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
val addProvision1 = modelVorhabenConsent.provision.addProvision()
addProvision1.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("permit"))
addProvision1.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
addProvision1.code.addLast(
CodeableConcept(
Coding(
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
"Teilnahme",
"Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung"
)
)
)
val addProvision2 = modelVorhabenConsent.provision.addProvision()
addProvision2.setType(org.hl7.fhir.r4.model.Consent.ConsentProvisionType.fromCode("deny"))
addProvision2.period.start = Date.from(Instant.parse("2025-06-23T00:00:00.00Z"))
addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z"))
addProvision2.code.addLast(
CodeableConcept(
Coding(
"https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV",
"Rekontaktierung",
"Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt"
)
)
)
return modelVorhabenConsent
}
}
}