diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt index 130fea7..1206c99 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt @@ -102,6 +102,18 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() { .id("TEST_12345678") .build() ) + .metadata( + MvhMetadata + .builder() + .modelProjectConsent( + ModelProjectConsent + .builder() + .provisions( + listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) + ).build() + ) + .build() + ) .diagnoses( listOf( MtbDiagnosis.builder() 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 9db509c..66b62c8 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt @@ -20,8 +20,9 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.ConsentEvaluator import dev.dnpm.etl.processor.consent.GicsConsentService +import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.input.KafkaInputListener import dev.dnpm.etl.processor.monitoring.RequestRepository import dev.dnpm.etl.processor.output.KafkaMtbFileSender @@ -53,7 +54,8 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean AppSecurityConfiguration::class, KafkaAutoConfiguration::class, AppKafkaConfiguration::class, - AppRestConfiguration::class + AppRestConfiguration::class, + ConsentEvaluator::class ] ) @MockitoBean(types = [ObjectMapper::class]) @@ -281,7 +283,8 @@ class AppConfigurationTest { @Nested @TestPropertySource( properties = [ - "app.consent.service=GICS" + "app.consent.service=GICS", + "app.consent.gics.uri=http://localhost:9000", ] ) inner class AppConfigurationConsentGicsTest(private val context: ApplicationContext) { @@ -293,27 +296,12 @@ class AppConfigurationTest { } - @Nested - @TestPropertySource( - properties = [ - "app.consent.gics.enabled=true" - ] - ) - inner class AppConfigurationConsentGicsEnabledTest(private val context: ApplicationContext) { - - @Test - fun shouldUseConfiguredGenerator() { - assertThat(context.getBean(GicsConsentService::class.java)).isNotNull - } - - } - @Nested inner class AppConfigurationConsentBuildinTest(private val context: ApplicationContext) { @Test fun shouldUseConfiguredGenerator() { - assertThat(context.getBean(ConsentByMtbFile::class.java)).isNotNull + assertThat(context.getBean(MtbFileConsentService::class.java)).isNotNull } } 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 78bdc8f..5f2d7ca 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -20,11 +20,11 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.anyValueClass import dev.dnpm.etl.processor.config.AppSecurityConfiguration -import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.ConsentEvaluation +import dev.dnpm.etl.processor.consent.ConsentEvaluator +import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.consent.TtpConsentStatus -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 @@ -57,32 +57,37 @@ import java.util.* classes = [ MtbFileRestController::class, AppSecurityConfiguration::class, - ConsentByMtbFile::class, IGetConsent::class + MtbFileConsentService::class ] ) -@MockitoBean(types = [TokenRepository::class, RequestProcessor::class]) +@MockitoBean(types = [TokenRepository::class, RequestProcessor::class, ConsentEvaluator::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.consent.gics.enabled=false" + "app.security.enable-tokens=true" ] ) class MtbFileRestControllerTest { - private lateinit var mockMvc: MockMvc - - private lateinit var requestProcessor: RequestProcessor + lateinit var mockMvc: MockMvc + lateinit var requestProcessor: RequestProcessor + lateinit var consentEvaluator: ConsentEvaluator @BeforeEach fun setup( @Autowired mockMvc: MockMvc, - @Autowired requestProcessor: RequestProcessor + @Autowired requestProcessor: RequestProcessor, + @Autowired consentEvaluator: ConsentEvaluator ) { this.mockMvc = mockMvc this.requestProcessor = requestProcessor + this.consentEvaluator = consentEvaluator + + doAnswer { + ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true) + }.whenever(consentEvaluator).check(any()) } @Test @@ -167,8 +172,7 @@ class MtbFileRestControllerTest { "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", "app.security.enable-tokens=true", - "app.security.enable-oidc=true", - "app.consent.gics.enabled=false" + "app.security.enable-oidc=true" ] ) inner class WithOidcEnabled { diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java index 6d0b160..51bfd50 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentDomain.java @@ -4,10 +4,10 @@ public enum ConsentDomain { /** * MII Broad consent */ - BroadConsent, + BROAD_CONSENT, /** - * GenomDe Modelvohaben §64e + * GenomDe Modellvorhaben §64e */ - Modelvorhaben64e + MODELLVORHABEN_64E } diff --git a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java index 6f3c987..95e8e8f 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -4,17 +4,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.DataFormatException; import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.GIcsConfigProperties; -import java.util.Date; 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.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.*; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; -import org.hl7.fhir.r4.model.StringType; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,112 +14,152 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.retry.TerminatedRetryException; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.net.URI; +import java.util.Date; -public class GicsConsentService implements IGetConsent { +/** + * Service to request Consent from remote gICS installation + * + * @since 0.11 + */ +public class GicsConsentService implements IConsentService { private final Logger log = LoggerFactory.getLogger(GicsConsentService.class); public static final String IS_CONSENTED_ENDPOINT = "/$isConsented"; public static final String IS_POLICY_STATES_FOR_PERSON_ENDPOINT = "/$currentPolicyStatesForPerson"; + private final RetryTemplate retryTemplate; private final RestTemplate restTemplate; private final FhirContext fhirContext; - private final HttpHeaders httpHeader; private final GIcsConfigProperties gIcsConfigProperties; - private String url; - - public GicsConsentService(GIcsConfigProperties gIcsConfigProperties, - RetryTemplate retryTemplate, RestTemplate restTemplate, AppFhirConfig appFhirConfig) { + public GicsConsentService( + GIcsConfigProperties gIcsConfigProperties, + RetryTemplate retryTemplate, + RestTemplate restTemplate, + AppFhirConfig appFhirConfig + ) { this.retryTemplate = retryTemplate; this.restTemplate = restTemplate; this.fhirContext = appFhirConfig.fhirContext(); - httpHeader = buildHeader(gIcsConfigProperties.getUsername(), - gIcsConfigProperties.getPassword()); this.gIcsConfigProperties = gIcsConfigProperties; log.info("GicsConsentService initialized..."); } - public String getGicsUri(String endpoint) { - if (url == null) { - final String gIcsBaseUri = gIcsConfigProperties.getUri(); - if (StringUtils.isBlank(gIcsBaseUri)) { - throw new IllegalArgumentException( - "gICS base URL is empty - should call gICS with false configuration."); - } - url = UriComponentsBuilder.fromUriString(gIcsBaseUri).path(endpoint) - .toUriString(); - } - return url; - } - - @NotNull - private static HttpHeaders buildHeader(String gPasUserName, String gPasPassword) { - var headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_XML); - - if (StringUtils.isBlank(gPasUserName) || StringUtils.isBlank(gPasPassword)) { - return headers; - } - - headers.setBasicAuth(gPasUserName, gPasPassword); - return headers; - } - - protected static Parameters getIsConsentedRequestParam(GIcsConfigProperties configProperties, - String personIdentifierValue) { + protected Parameters getFhirRequestParameters( + String personIdentifierValue + ) { var result = new Parameters(); - result.addParameter(new ParametersParameterComponent().setName("personIdentifier").setValue( - new Identifier().setValue(personIdentifierValue) - .setSystem(configProperties.getPersonIdentifierSystem()))); - result.addParameter(new ParametersParameterComponent().setName("domain") - .setValue(new StringType().setValue(configProperties.getBroadConsentDomainName()))); - result.addParameter(new ParametersParameterComponent().setName("policy").setValue( - new Coding().setCode(configProperties.getBroadConsentPolicyCode()) - .setSystem(configProperties.getBroadConsentPolicySystem()))); + result.addParameter( + new ParametersParameterComponent() + .setName("personIdentifier") + .setValue( + new Identifier() + .setValue(personIdentifierValue) + .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem()) + ) + ); + result.addParameter( + new ParametersParameterComponent() + .setName("domain") + .setValue( + new StringType() + .setValue(this.gIcsConfigProperties.getBroadConsentDomainName()) + ) + ); + result.addParameter( + new ParametersParameterComponent() + .setName("policy") + .setValue( + new Coding() + .setCode(this.gIcsConfigProperties.getBroadConsentPolicyCode()) + .setSystem(this.gIcsConfigProperties.getBroadConsentPolicySystem()) + ) + ); /* * is mandatory parameter, but we ignore it via additional configuration parameter * 'ignoreVersionNumber'. */ - result.addParameter(new ParametersParameterComponent().setName("version") - .setValue(new StringType().setValue("1.1"))); + result.addParameter( + new ParametersParameterComponent() + .setName("version") + .setValue(new StringType().setValue("1.1") + ) + ); /* add config parameter with: * ignoreVersionNumber -> true ->> Reason is we cannot know which policy version each patient * has possibly signed or not, therefore we are happy with any version found. * unknownStateIsConsideredAsDecline -> true */ - var config = new ParametersParameterComponent().setName("config").addPart( - new ParametersParameterComponent().setName("ignoreVersionNumber") - .setValue(new BooleanType().setValue(true))).addPart( - new ParametersParameterComponent().setName("unknownStateIsConsideredAsDecline") - .setValue(new BooleanType().setValue(false))); + var config = new ParametersParameterComponent() + .setName("config") + .addPart( + new ParametersParameterComponent() + .setName("ignoreVersionNumber") + .setValue(new BooleanType().setValue(true)) + ) + .addPart( + new ParametersParameterComponent() + .setName("unknownStateIsConsideredAsDecline") + .setValue(new BooleanType().setValue(false)) + ); + result.addParameter(config); return result; } + private URI endpointUri(String endpoint) { + assert this.gIcsConfigProperties.getUri() != null; + return UriComponentsBuilder.fromUriString(this.gIcsConfigProperties.getUri()).path(endpoint).build().toUri(); + } + + private HttpHeaders headersWithHttpBasicAuth() { + assert this.gIcsConfigProperties.getUri() != null; + + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + + if ( + StringUtils.isBlank(this.gIcsConfigProperties.getUsername()) + || StringUtils.isBlank(this.gIcsConfigProperties.getPassword()) + ) { + return headers; + } + + headers.setBasicAuth(this.gIcsConfigProperties.getUsername(), this.gIcsConfigProperties.getPassword()); + return headers; + } + protected String callGicsApi(Parameters parameter, String endpoint) { var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); - - HttpEntity requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader); - ResponseEntity responseEntity; + HttpEntity requestEntity = new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth()); try { - var url = getGicsUri(endpoint); + var responseEntity = retryTemplate.execute( + ctx -> restTemplate.exchange(endpointUri(endpoint), HttpMethod.POST, requestEntity, String.class) + ); - responseEntity = retryTemplate.execute( - ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class)); + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity.getBody(); + } else { + var msg = String.format( + "Trusted party system reached but request failed! code: '%s' response: '%s'", + responseEntity.getStatusCode(), responseEntity.getBody()); + log.error(msg); + return null; + } } catch (RestClientException e) { var msg = String.format("Get consents status request failed reason: '%s", - e.getMessage()); + e.getMessage()); log.error(msg); return null; @@ -137,39 +169,32 @@ public class GicsConsentService implements IGetConsent { terminatedRetryException.getMessage()); log.error(msg); return null; - - } - if (responseEntity.getStatusCode().is2xxSuccessful()) { - return responseEntity.getBody(); - } else { - var msg = String.format( - "Trusted party system reached but request failed! code: '%s' response: '%s'", - responseEntity.getStatusCode(), responseEntity.getBody()); - log.error(msg); - return null; } } @Override public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { - var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties, - personIdentifierValue); - - var consentStatusResponse = callGicsApi(parameter, - GicsConsentService.IS_CONSENTED_ENDPOINT); + var consentStatusResponse = callGicsApi( + getFhirRequestParameters(personIdentifierValue), + GicsConsentService.IS_CONSENTED_ENDPOINT + ); return evaluateConsentResponse(consentStatusResponse); } - protected Bundle currentConsentForPersonAndTemplate(String personIdentifierValue, - ConsentDomain targetConsentDomain, Date requestDate) { + protected Bundle currentConsentForPersonAndTemplate( + String personIdentifierValue, + ConsentDomain consentDomain, + Date requestDate + ) { - String consentDomain = getConsentDomain(targetConsentDomain); - - var requestParameter = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( - gIcsConfigProperties, personIdentifierValue, requestDate, consentDomain); + var requestParameter = buildRequestParameterCurrentPolicyStatesForPerson( + personIdentifierValue, + requestDate, + consentDomain + ); var consentDataSerialized = callGicsApi(requestParameter, - GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); + GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); if (consentDataSerialized == null) { // error occurred - should not process further! @@ -177,15 +202,15 @@ public class GicsConsentService implements IGetConsent { "consent data request failed - stopping processing! - try again or fix other problems first."); } var iBaseResource = fhirContext.newJsonParser() - .parseResource(consentDataSerialized); + .parseResource(consentDataSerialized); if (iBaseResource instanceof OperationOutcome) { // log error - very likely a configuration error String errorMessage = "Consent request failed! Check outcome:\n " + consentDataSerialized; log.error(errorMessage); throw new IllegalStateException(errorMessage); - } else if (iBaseResource instanceof Bundle) { - return (Bundle) iBaseResource; + } else if (iBaseResource instanceof Bundle bundle) { + return bundle; } else { String errorMessage = "Consent request failed! Unexpected response received! -> " + consentDataSerialized; @@ -195,40 +220,52 @@ public class GicsConsentService implements IGetConsent { } @NotNull - private String getConsentDomain(ConsentDomain targetConsentDomain) { - String consentDomain; - switch (targetConsentDomain) { - case BroadConsent -> consentDomain = gIcsConfigProperties.getBroadConsentDomainName(); - case Modelvorhaben64e -> - consentDomain = gIcsConfigProperties.getGenomDeConsentDomainName(); - default -> throw new IllegalArgumentException( - "target ConsentDomain is missing but must be provided!"); - } - return consentDomain; + private String getConsentDomainName(ConsentDomain targetConsentDomain) { + return switch (targetConsentDomain) { + case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName(); + case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName(); + }; } - protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson( - GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate, - String targetDomain) { + protected Parameters buildRequestParameterCurrentPolicyStatesForPerson( + String personIdentifierValue, + Date requestDate, + ConsentDomain consentDomain + ) { var requestParameter = new Parameters(); - requestParameter.addParameter(new ParametersParameterComponent().setName("personIdentifier") - .setValue(new Identifier().setValue(personIdentifierValue) - .setSystem(gIcsConfigProperties.getPersonIdentifierSystem()))); + requestParameter.addParameter( + new ParametersParameterComponent() + .setName("personIdentifier") + .setValue( + new Identifier() + .setValue(personIdentifierValue) + .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem()) + ) + ); - requestParameter.addParameter(new ParametersParameterComponent().setName("domain") - .setValue(new StringType().setValue(targetDomain))); + requestParameter.addParameter( + new ParametersParameterComponent() + .setName("domain") + .setValue(new StringType().setValue(getConsentDomainName(consentDomain))) + ); Parameters nestedConfigParameters = new Parameters(); - nestedConfigParameters.addParameter( - new ParametersParameterComponent().setName("idMatchingType").setValue( - new Coding().setSystem( - "https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType") - .setCode("AT_LEAST_ONE"))).addParameter("ignoreVersionNumber", false) + nestedConfigParameters + .addParameter( + new ParametersParameterComponent() + .setName("idMatchingType") + .setValue(new Coding() + .setSystem("https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType") + .setCode("AT_LEAST_ONE") + ) + ) + .addParameter("ignoreVersionNumber", false) .addParameter("unknownStateIsConsideredAsDecline", false) .addParameter("requestDate", new DateType().setValue(requestDate)); - requestParameter.addParameter(new ParametersParameterComponent().setName("config").addPart() - .setResource(nestedConfigParameters)); + requestParameter.addParameter( + new ParametersParameterComponent().setName("config").addPart().setResource(nestedConfigParameters) + ); return requestParameter; } @@ -254,7 +291,7 @@ public class GicsConsentService implements IGetConsent { } } else if (response instanceof OperationOutcome outcome) { log.error("failed to get consent status from ttp. probably configuration error. " - + "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome)); + + "outcome: '{}'", fhirContext.newJsonParser().encodeToString(outcome)); } } catch (DataFormatException dfe) { @@ -265,17 +302,6 @@ public class GicsConsentService implements IGetConsent { @Override public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) { - switch (consentDomain) { - case BroadConsent -> { - return currentConsentForPersonAndTemplate(patientId, ConsentDomain.BroadConsent, - requestDate); - } - case Modelvorhaben64e -> { - return currentConsentForPersonAndTemplate(patientId, - ConsentDomain.Modelvorhaben64e, requestDate); - } - } - - return new Bundle(); + return currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate); } } diff --git a/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java similarity index 96% rename from src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java rename to src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java index 3482b9a..ded3515 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/IGetConsent.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/IConsentService.java @@ -3,7 +3,7 @@ package dev.dnpm.etl.processor.consent; import java.util.Date; import org.hl7.fhir.r4.model.Bundle; -public interface IGetConsent { +public interface IConsentService { /** * Get broad consent status for a patient identifier diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java similarity index 76% rename from src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java rename to src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java index f7ce39e..24cb8f7 100644 --- a/src/main/java/dev/dnpm/etl/processor/consent/ConsentByMtbFile.java +++ b/src/main/java/dev/dnpm/etl/processor/consent/MtbFileConsentService.java @@ -5,11 +5,11 @@ import org.hl7.fhir.r4.model.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ConsentByMtbFile implements IGetConsent { +public class MtbFileConsentService implements IConsentService { - private static final Logger log = LoggerFactory.getLogger(ConsentByMtbFile.class); + private static final Logger log = LoggerFactory.getLogger(MtbFileConsentService.class); - public ConsentByMtbFile() { + public MtbFileConsentService() { log.info("ConsentCheckFileBased initialized..."); } 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 1f3c650..5dea8ab 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -73,8 +73,8 @@ data class GIcsConfigProperties( * */ val uri: String?, - val username: String?, - val password: String?, + val username: String? = null, + val password: String? = null, /** * gICS specific system 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 8f90947..f32ecaa 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -20,9 +20,9 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.consent.GicsConsentService -import dev.dnpm.etl.processor.consent.IGetConsent +import dev.dnpm.etl.processor.consent.IConsentService import dev.dnpm.etl.processor.monitoring.* import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator import dev.dnpm.etl.processor.pseudonym.Generator @@ -218,7 +218,7 @@ class AppConfiguration { retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig - ): IGetConsent { + ): IConsentService { return GicsConsentService( gIcsConfigProperties, retryTemplate, @@ -234,7 +234,7 @@ class AppConfiguration { gIcsConfigProperties: GIcsConfigProperties, getObjectMapper: ObjectMapper, appFhirConfig: AppFhirConfig, - gicsConsentService: IGetConsent + gicsConsentService: IConsentService ): ConsentProcessor { return ConsentProcessor( configProperties, @@ -261,8 +261,8 @@ class AppConfiguration { @Bean @ConditionalOnMissingBean - fun iGetConsentService(): IGetConsent { - return ConsentByMtbFile() + fun iGetConsentService(): IConsentService { + return MtbFileConsentService() } } @@ -271,13 +271,9 @@ class GicsEnabledCondition : AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) { @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics") + @ConditionalOnProperty(name = ["app.consent.gics.uri"]) class OnGicsServiceSelected { // Just for Condition } - @ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true") - class OnGicsEnabled { - // Just for Condition - } - } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt index de11cbb..6551713 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt @@ -20,6 +20,7 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper +import dev.dnpm.etl.processor.consent.ConsentEvaluator import dev.dnpm.etl.processor.input.KafkaInputListener import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult import dev.dnpm.etl.processor.monitoring.ConnectionCheckService @@ -100,9 +101,10 @@ class AppKafkaConfiguration { @ConditionalOnProperty(value = ["app.kafka.input-topic"]) fun kafkaInputListener( requestProcessor: RequestProcessor, - objectMapper: ObjectMapper + objectMapper: ObjectMapper, + consentEvaluator: ConsentEvaluator ): KafkaInputListener { - return KafkaInputListener(requestProcessor, objectMapper) + return KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) } @Bean @@ -113,4 +115,4 @@ class AppKafkaConfiguration { return KafkaConnectionCheckService(consumerFactory.createConsumer(), connectionCheckUpdateProducer) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt b/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt new file mode 100644 index 0000000..195346d --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt @@ -0,0 +1,66 @@ +/* + * This file is part of ETL-Processor + * + * Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.dnpm.etl.processor.consent + +import dev.pcvolkmer.mv64e.mtb.ConsentProvision +import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose +import dev.pcvolkmer.mv64e.mtb.Mtb +import org.springframework.stereotype.Service + +/** + * Evaluates consent using provided consent service and file based consent information + */ +@Service +class ConsentEvaluator( + private val consentService: IConsentService +) { + fun check(mtbFile: Mtb): ConsentEvaluation { + val ttpConsentStatus = consentService.getTtpBroadConsentStatus(mtbFile.patient.id) + val consentGiven = ttpConsentStatus == TtpConsentStatus.BROAD_CONSENT_GIVEN + || ttpConsentStatus == TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT + // Aktuell nur Modellvorhaben Consent im File + || ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE && mtbFile.metadata?.modelProjectConsent?.provisions?.any { + it.purpose == ModelProjectConsentPurpose.SEQUENCING + && it.type == ConsentProvision.PERMIT + } == true + + return ConsentEvaluation(ttpConsentStatus, consentGiven) + } +} + +data class ConsentEvaluation(private val ttpConsentStatus: TtpConsentStatus, private val consentGiven: Boolean) { + /** + * Checks if any required consent is present + */ + fun hasConsent(): Boolean { + return consentGiven + } + + /** + * Returns the consent status + */ + fun getStatus(): TtpConsentStatus { + if (ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE) { + // in case ttp check is disabled - we propagate rejected status anyway + return TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED + } + return ttpConsentStatus + } +} 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 47615be..4ac9f2d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt @@ -23,9 +23,9 @@ import com.fasterxml.jackson.databind.ObjectMapper import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.RequestId +import dev.dnpm.etl.processor.consent.ConsentEvaluator import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor -import dev.pcvolkmer.mv64e.mtb.ConsentProvision import dev.pcvolkmer.mv64e.mtb.Mtb import org.apache.kafka.clients.consumer.ConsumerRecord import org.slf4j.LoggerFactory @@ -34,6 +34,7 @@ import org.springframework.kafka.listener.MessageListener class KafkaInputListener( private val requestProcessor: RequestProcessor, + private val consentEvaluator: ConsentEvaluator, private val objectMapper: ObjectMapper ) : MessageListener { private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java) @@ -70,8 +71,7 @@ class KafkaInputListener( RequestId("") } - // TODO: Use MV Consent for now - needs to be replaced with proper consent evaluation - if (mtbFile.metadata.modelProjectConsent.provisions.filter { it.type == ConsentProvision.PERMIT }.isNotEmpty()) { + if (consentEvaluator.check(mtbFile).hasConsent()) { logger.debug("Accepted MTB File for processing") if (requestId.isBlank()) { requestProcessor.processMtbFile(mtbFile) 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 d00ad25..e154536 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -21,7 +21,7 @@ package dev.dnpm.etl.processor.input import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.PatientId -import dev.dnpm.etl.processor.consent.IGetConsent +import dev.dnpm.etl.processor.consent.ConsentEvaluator import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.Mtb @@ -34,9 +34,8 @@ import org.springframework.web.bind.annotation.* @RequestMapping(path = ["mtbfile", "mtb"]) class MtbFileRestController( private val requestProcessor: RequestProcessor, - private val iGetConsent: IGetConsent + private val consentEvaluator: ConsentEvaluator ) { - private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) @GetMapping @@ -46,8 +45,15 @@ class MtbFileRestController( @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE]) fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity { - logger.debug("Accepted MTB File (DNPM V2) for processing") - requestProcessor.processMtbFile(mtbFile) + val consentEvaluation = consentEvaluator.check(mtbFile) + if (consentEvaluation.hasConsent()) { + logger.debug("Accepted MTB File (DNPM V2) for processing") + requestProcessor.processMtbFile(mtbFile) + } else { + logger.debug("Accepted MTB File (DNPM V2) and process deletion") + val patientId = PatientId(mtbFile.patient.id) + requestProcessor.processDeletion(patientId, consentEvaluation.getStatus()) + } return ResponseEntity.accepted().build() } 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 11aff57..6688087 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt @@ -6,9 +6,9 @@ import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.config.GIcsConfigProperties -import dev.dnpm.etl.processor.consent.ConsentByMtbFile +import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.consent.ConsentDomain -import dev.dnpm.etl.processor.consent.IGetConsent +import dev.dnpm.etl.processor.consent.IConsentService import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized import dev.pcvolkmer.mv64e.mtb.* import org.apache.commons.lang3.NotImplementedException @@ -31,7 +31,7 @@ class ConsentProcessor( private val gIcsConfigProperties: GIcsConfigProperties, private val objectMapper: ObjectMapper, private val fhirContext: FhirContext, - private val consentService: IGetConsent + private val consentService: IConsentService ) { private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor") @@ -49,7 +49,7 @@ class ConsentProcessor( * */ fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { - if (consentService is ConsentByMtbFile) { + if (consentService is MtbFileConsentService) { // consent check is disabled return true } @@ -70,7 +70,7 @@ class ConsentProcessor( * broad consent */ val broadConsent = consentService.getConsent( - personIdentifierValue, requestDate, ConsentDomain.BroadConsent + personIdentifierValue, requestDate, ConsentDomain.BROAD_CONSENT ) val broadConsentHasBeenAsked = !broadConsent.entry.isEmpty() @@ -78,7 +78,7 @@ class ConsentProcessor( if (!broadConsentHasBeenAsked) return false val genomeDeConsent = consentService.getConsent( - personIdentifierValue, requestDate, ConsentDomain.Modelvorhaben64e + personIdentifierValue, requestDate, ConsentDomain.MODELLVORHABEN_64E ) addGenomeDbProvisions(mtbFile, genomeDeConsent) @@ -88,11 +88,11 @@ class ConsentProcessor( embedBroadConsentResources(mtbFile, broadConsent) val broadConsentStatus = getProvisionTypeByPolicyCode( - broadConsent, requestDate, ConsentDomain.BroadConsent + broadConsent, requestDate, ConsentDomain.BROAD_CONSENT ) val genomDeSequencingStatus = getProvisionTypeByPolicyCode( - genomeDeConsent, requestDate, ConsentDomain.Modelvorhaben64e + genomeDeConsent, requestDate, ConsentDomain.MODELLVORHABEN_64E ) if (Consent.ConsentProvisionType.NULL == broadConsentStatus) { @@ -204,10 +204,10 @@ class ConsentProcessor( ): Consent.ConsentProvisionType { val code: String? val system: String? - if (ConsentDomain.BroadConsent == consentDomain) { + if (ConsentDomain.BROAD_CONSENT == consentDomain) { code = gIcsConfigProperties.broadConsentPolicyCode system = gIcsConfigProperties.broadConsentPolicySystem - } else if (ConsentDomain.Modelvorhaben64e == consentDomain) { + } else if (ConsentDomain.MODELLVORHABEN_64E == consentDomain) { code = gIcsConfigProperties.genomeDePolicyCode system = gIcsConfigProperties.genomeDePolicySystem } else { @@ -279,4 +279,4 @@ class ConsentProcessor( return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0 } -} \ No newline at end of file +} diff --git a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java index d26eca2..6fa8f08 100644 --- a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java +++ b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java @@ -1,85 +1,112 @@ 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 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.time.Instant; -import java.util.Date; -import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; import org.hl7.fhir.r4.model.OperationOutcome.IssueType; import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; -import org.hl7.fhir.r4.model.Parameters; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.http.MediaType; +import org.springframework.retry.support.RetryTemplate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +import java.time.Instant; +import java.util.Date; + +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.withServerError; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class}) -@TestPropertySource(properties = {"app.consent.gics.enabled=true", - "app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"}) +@TestPropertySource(properties = { + "app.consent.service=gics", + "app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics" +}) @RestClientTest -public class GicsConsentServiceTest { +class GicsConsentServiceTest { + + static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics"; - public static final String GICS_BASE_URI = "http://localhost:8090/ttp-fhir/fhir/gics"; - @Autowired MockRestServiceServer mockRestServiceServer; - - @Autowired - GicsConsentService gicsConsentService; - - @Autowired - AppConfiguration appConfiguration; - - @Autowired AppFhirConfig appFhirConfig; - - @Autowired GIcsConfigProperties gIcsConfigProperties; + GicsConsentService gicsConsentService; + @BeforeEach - public void setUp() { - mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate()); + void setUp( + @Autowired AppFhirConfig appFhirConfig, + @Autowired GIcsConfigProperties gIcsConfigProperties + ) { + this.appFhirConfig = appFhirConfig; + this.gIcsConfigProperties = gIcsConfigProperties; + + var restTemplate = new RestTemplate(); + + this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate); + this.gicsConsentService = new GicsConsentService( + this.gIcsConfigProperties, + RetryTemplate.builder().maxAttempts(1).build(), + restTemplate, + this.appFhirConfig + ); } @Test - void getTtpBroadConsentStatus() { - final Parameters responseConsented = new Parameters().addParameter( - new ParametersParameterComponent().setName("consented") - .setValue(new BooleanType().setValue(true))); + void shouldReturnTtpBroadConsentStatus() { + final Parameters consentedResponse = new Parameters() + .addParameter( + 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(consentedResponse), + MediaType.APPLICATION_JSON + ) + ); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN); } @Test - void consentRevoced() { - final Parameters responseRevoced = new Parameters().addParameter( - new ParametersParameterComponent().setName("consented") - .setValue(new BooleanType().setValue(false))); + void shouldReturnRevokedConsent() { + final Parameters revokedResponse = new Parameters() + .addParameter( + 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), - MediaType.APPLICATION_JSON)); + mockRestServiceServer + .expect( + requestTo( + "http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT) + ) + .andRespond( + withSuccess( + appFhirConfig.fhirContext().newJsonParser().encodeResourceToString(revokedResponse), + MediaType.APPLICATION_JSON) + ); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED); @@ -87,15 +114,39 @@ public class GicsConsentServiceTest { @Test - void gicsParameterInvalid() { - final OperationOutcome responseErrorOutcome = new OperationOutcome().addIssue( - new OperationOutcomeIssueComponent().setSeverity(IssueSeverity.ERROR) - .setCode(IssueType.PROCESSING).setDiagnostics("Invalid policy parameter...")); + void shouldReturnInvalidParameterResponse() { + final OperationOutcome responseWithErrorOutcome = new OperationOutcome() + .addIssue( + 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)); + mockRestServiceServer + .expect( + requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT) + ) + .andRespond( + withSuccess( + appFhirConfig.fhirContext().newJsonParser().encodeResourceToString(responseWithErrorOutcome), + MediaType.APPLICATION_JSON + ) + ); + + var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); + assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK); + } + + @Test + void shouldReturnRequestError() { + mockRestServiceServer + .expect( + requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT) + ) + .andRespond( + withServerError() + ); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK); @@ -103,20 +154,27 @@ public class GicsConsentServiceTest { @Test void buildRequestParameterCurrentPolicyStatesForPersonTest() { - String pid = "12345678"; - var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( - gIcsConfigProperties, pid, Date.from(Instant.now()), - gIcsConfigProperties.getGenomDeConsentDomainName()); + var result = gicsConsentService + .buildRequestParameterCurrentPolicyStatesForPerson( + pid, + Date.from(Instant.now()), + ConsentDomain.MODELLVORHABEN_64E + ); - assertThat(result.getParameter().size()).as("should contain 3 parameter resources") - .isEqualTo(3); + assertThat(result.getParameter()) + .as("should contain 3 parameter resources") + .hasSize(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 + ); } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/consent/Dnpm21BasedConsentEvaluatorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/consent/Dnpm21BasedConsentEvaluatorTest.kt new file mode 100644 index 0000000..adbec2f --- /dev/null +++ b/src/test/kotlin/dev/dnpm/etl/processor/consent/Dnpm21BasedConsentEvaluatorTest.kt @@ -0,0 +1,287 @@ +/* + * This file is part of ETL-Processor + * + * Copyright (c) 2025 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.dnpm.etl.processor.consent + +import dev.dnpm.etl.processor.ArgProvider +import dev.pcvolkmer.mv64e.mtb.* +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsSource +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.whenever +import java.time.Instant +import java.util.* + +@ExtendWith(MockitoExtension::class) +class Dnpm21BasedConsentEvaluatorTest { + + @Nested + inner class WithGicsConsentEnabled { + + lateinit var consentService: GicsConsentService + lateinit var consentEvaluator: ConsentEvaluator + + @BeforeEach + fun setUp( + @Mock consentService: GicsConsentService + ) { + this.consentService = consentService + this.consentEvaluator = ConsentEvaluator(consentService) + } + + @ParameterizedTest + @ArgumentsSource(WithGicsMtbFileProvider::class) + fun test( + mtbFile: Mtb, + ttpConsentStatus: TtpConsentStatus, + expectedConsentEvaluation: ConsentEvaluation + ) { + whenever(consentService.getTtpBroadConsentStatus(anyString())).thenReturn( + ttpConsentStatus + ) + assertThat(consentEvaluator.check(mtbFile)).isEqualTo(expectedConsentEvaluation) + } + } + + @Nested + inner class WithFileConsentOnly { + + lateinit var consentService: MtbFileConsentService + lateinit var consentEvaluator: ConsentEvaluator + + @BeforeEach + fun setUp() { + this.consentService = MtbFileConsentService() + this.consentEvaluator = ConsentEvaluator(consentService) + } + + @ParameterizedTest + @ArgumentsSource(MtbFileProvider::class) + fun test(mtbFile: Mtb, expectedConsentEvaluation: ConsentEvaluation) { + assertThat(consentEvaluator.check(mtbFile)).isEqualTo(expectedConsentEvaluation) + } + } + + // Util classes + + class WithGicsMtbFileProvider : ArgProvider( + // Has file ModelProjectConsent and broad consent => consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.BROAD_CONSENT_GIVEN, + ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true) + ), + // Has file ModelProjectConsent and broad consent missing => no consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.BROAD_CONSENT_MISSING, + ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING, false) + ), + // Has file ModelProjectConsent and broad consent missing or rejected => no consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, + ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, false) + ), + // Has file ModelProjectConsent and MV consent => consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, + ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, true) + ), + // Has file ModelProjectConsent and MV consent rejected => no consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED, + ConsentEvaluation(TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED, false) + ), + // Has file ModelProjectConsent and MV consent missing => no consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.GENOM_DE_CONSENT_MISSING, + ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_MISSING, false) + ), + // Has file ModelProjectConsent and no broad consent result => consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.UNKNOWN_CHECK_FILE, + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, true) + ), + // Has file ModelProjectConsent and failed to ask => no consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + TtpConsentStatus.FAILED_TO_ASK, + ConsentEvaluation(TtpConsentStatus.FAILED_TO_ASK, false) + ), + // File ModelProjectConsent rejected and broad consent => consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.BROAD_CONSENT_GIVEN, + ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true) + ), + // File ModelProjectConsent rejected and broad consent missing => no consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.BROAD_CONSENT_MISSING, + ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING, false) + ), + // File ModelProjectConsent rejected and broad consent missing or rejected => no consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, + ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, false) + ), + // File ModelProjectConsent rejected and MV consent => consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, + ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, true) + ), + // File ModelProjectConsent rejected and MV consent rejected => no consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED, + ConsentEvaluation(TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED, false) + ), + // File ModelProjectConsent rejected and MV consent missing => no consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.GENOM_DE_CONSENT_MISSING, + ConsentEvaluation(TtpConsentStatus.GENOM_DE_CONSENT_MISSING, false) + ), + // File ModelProjectConsent rejected and no broad consent result => no consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.UNKNOWN_CHECK_FILE, + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false) + ), + // File ModelProjectConsent rejected and failed to ask => no consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + TtpConsentStatus.FAILED_TO_ASK, + ConsentEvaluation(TtpConsentStatus.FAILED_TO_ASK, false) + ) + ) { + + companion object { + fun buildMtb(consentProvision: ConsentProvision): Mtb { + return Mtb.builder() + .patient( + Patient.builder().id("TEST_12345678") + .birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))).gender( + GenderCoding.builder().code(GenderCodingCode.MALE).build() + ).build() + ) + .metadata( + MvhMetadata.builder().modelProjectConsent( + ModelProjectConsent.builder().provisions( + listOf( + Provision.builder().date(Date()).type(consentProvision) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) + ).build() + ).build() + ) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder().id("1") + .patient(Reference.builder().id("TEST_12345678").build()) + .build() + ) + ) + .build() + } + } + } + + class MtbFileProvider : ArgProvider( + // Has file consent => consent given + Arguments.of( + buildMtb(ConsentProvision.PERMIT), + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, true) + ), + // File consent rejected => no consent given + Arguments.of( + buildMtb(ConsentProvision.DENY), + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false) + ), + // policy REIDENTIFICATION has no effect on ConsentEvaluation + Arguments.of( + buildMtb(ModelProjectConsentPurpose.REIDENTIFICATION, ConsentProvision.DENY), + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false) + ), Arguments.of( + buildMtb(ModelProjectConsentPurpose.REIDENTIFICATION, ConsentProvision.PERMIT), + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false) + ), + // policy CASE_IDENTIFICATION has no effect on ConsentEvaluation + Arguments.of( + buildMtb(ModelProjectConsentPurpose.CASE_IDENTIFICATION, ConsentProvision.DENY), + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false) + ), Arguments.of( + buildMtb(ModelProjectConsentPurpose.CASE_IDENTIFICATION, ConsentProvision.PERMIT), + ConsentEvaluation(TtpConsentStatus.UNKNOWN_CHECK_FILE, false) + ) + ) { + + companion object { + fun buildMtb(consentProvision: ConsentProvision): Mtb { + return buildMtb(ModelProjectConsentPurpose.SEQUENCING, consentProvision) + } + + fun buildMtb( + policy: ModelProjectConsentPurpose, + consentProvision: ConsentProvision + ): Mtb { + return Mtb.builder() + .patient( + Patient.builder().id("TEST_12345678") + .birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))).gender( + GenderCoding.builder().code(GenderCodingCode.MALE).build() + ).build() + ) + .metadata( + MvhMetadata.builder().modelProjectConsent( + ModelProjectConsent.builder().provisions( + listOf( + Provision.builder().date(Date()).type(consentProvision) + .purpose(policy).build() + ) + ).build() + ).build() + ) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder().id("1") + .patient(Reference.builder().id("TEST_12345678").build()) + .build() + ) + ) + .build() + } + } + } + +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt b/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt index 8caa908..2dfb1e1 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt @@ -17,4 +17,15 @@ * along with this program. If not, see . */ -package dev.dnpm.etl.processor \ No newline at end of file +package dev.dnpm.etl.processor + +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +open class ArgProvider(vararg val data: Arguments) : ArgumentsProvider { + override fun provideArguments( + context: ExtensionContext? + ): Stream = Stream.of(*data) +} 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 1239cdf..a047f74 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt @@ -20,8 +20,10 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper -import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.CustomMediaType +import dev.dnpm.etl.processor.consent.ConsentEvaluation +import dev.dnpm.etl.processor.consent.ConsentEvaluator +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.* import org.apache.kafka.clients.consumer.ConsumerRecord @@ -40,21 +42,32 @@ import java.util.* class KafkaInputListenerTest { private lateinit var requestProcessor: RequestProcessor + private lateinit var consentEvaluator: ConsentEvaluator private lateinit var objectMapper: ObjectMapper + private lateinit var kafkaInputListener: KafkaInputListener @BeforeEach fun setup( @Mock requestProcessor: RequestProcessor, + @Mock consentEvaluator: ConsentEvaluator, ) { this.requestProcessor = requestProcessor + this.consentEvaluator = consentEvaluator this.objectMapper = ObjectMapper() - this.kafkaInputListener = KafkaInputListener(requestProcessor, objectMapper) + this.kafkaInputListener = KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) } @Test fun shouldProcessMtbFileRequest() { + whenever(consentEvaluator.check(any())).thenReturn( + ConsentEvaluation( + TtpConsentStatus.BROAD_CONSENT_GIVEN, + true + ) + ) + val mtbFile = Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( @@ -64,7 +77,10 @@ class KafkaInputListenerTest { ModelProjectConsent .builder() .provisions( - listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) + listOf( + Provision.builder().type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) ).build() ) .build() @@ -86,6 +102,13 @@ class KafkaInputListenerTest { @Test fun shouldProcessDeleteRequest() { + whenever(consentEvaluator.check(any())).thenReturn( + ConsentEvaluation( + TtpConsentStatus.BROAD_CONSENT_GIVEN, + false + ) + ) + val mtbFile = Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( @@ -95,7 +118,10 @@ class KafkaInputListenerTest { ModelProjectConsent .builder() .provisions( - listOf(Provision.builder().type(ConsentProvision.DENY).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) + listOf( + Provision.builder().type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) ).build() ) .build() @@ -120,6 +146,13 @@ class KafkaInputListenerTest { @Test fun shouldProcessMtbFileRequestWithExistingRequestId() { + whenever(consentEvaluator.check(any())).thenReturn( + ConsentEvaluation( + TtpConsentStatus.BROAD_CONSENT_GIVEN, + true + ) + ) + val mtbFile = Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( @@ -129,7 +162,10 @@ class KafkaInputListenerTest { ModelProjectConsent .builder() .provisions( - listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) + listOf( + Provision.builder().type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) ).build() ) .build() @@ -158,6 +194,13 @@ class KafkaInputListenerTest { @Test fun shouldProcessDeleteRequestWithExistingRequestId() { + whenever(consentEvaluator.check(any())).thenReturn( + ConsentEvaluation( + TtpConsentStatus.BROAD_CONSENT_GIVEN, + false + ) + ) + val mtbFile = Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( @@ -167,7 +210,10 @@ class KafkaInputListenerTest { ModelProjectConsent .builder() .provisions( - listOf(Provision.builder().type(ConsentProvision.DENY).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) + listOf( + Provision.builder().type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) ).build() ) .build() @@ -208,7 +254,10 @@ class KafkaInputListenerTest { ModelProjectConsent .builder() .provisions( - listOf(Provision.builder().type(ConsentProvision.DENY).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) + listOf( + Provision.builder().type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) ).build() ) .build() 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 845f325..ae9e4e2 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -20,23 +20,34 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper +import dev.dnpm.etl.processor.ArgProvider import dev.dnpm.etl.processor.CustomMediaType -import dev.dnpm.etl.processor.consent.GicsConsentService +import dev.dnpm.etl.processor.consent.ConsentEvaluation +import dev.dnpm.etl.processor.consent.ConsentEvaluator +import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor -import dev.pcvolkmer.mv64e.mtb.Mtb +import dev.pcvolkmer.mv64e.mtb.* 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.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsSource import org.mockito.Mock 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.anyValueClass +import org.mockito.kotlin.whenever import org.springframework.core.io.ClassPathResource import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.post import org.springframework.test.web.servlet.setup.MockMvcBuilders +import java.time.Instant +import java.util.* @ExtendWith(MockitoExtension::class) class MtbFileRestControllerTest { @@ -49,22 +60,31 @@ class MtbFileRestControllerTest { private lateinit var mockMvc: MockMvc private lateinit var requestProcessor: RequestProcessor + private lateinit var consentEvaluator: ConsentEvaluator @BeforeEach fun setup( @Mock requestProcessor: RequestProcessor, - @Mock gicsConsentService: GicsConsentService + @Mock consentEvaluator: ConsentEvaluator ) { this.requestProcessor = requestProcessor + this.consentEvaluator = consentEvaluator val controller = MtbFileRestController( requestProcessor, - gicsConsentService + consentEvaluator ) this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() } @Test fun shouldRespondPostRequest() { + whenever(consentEvaluator.check(any())).thenReturn( + ConsentEvaluation( + TtpConsentStatus.BROAD_CONSENT_GIVEN, + true + ) + ) + val mtbFileContent = ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8) @@ -80,5 +100,127 @@ class MtbFileRestControllerTest { verify(requestProcessor, times(1)).processMtbFile(any()) } + @ParameterizedTest + @ArgumentsSource(Dnpm21MtbFile::class) + fun shouldProcessPostRequest(mtb: Mtb, broadConsent: TtpConsentStatus, shouldProcess: String) { + whenever(consentEvaluator.check(any())).thenReturn( + ConsentEvaluation( + broadConsent, + shouldProcess == "process" + ) + ) + + mockMvc.post("/mtbfile") { + content = objectMapper.writeValueAsString(mtb) + contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + }.andExpect { + status { + isAccepted() + } + } + + if (shouldProcess == "process") { + verify(requestProcessor, times(1)).processMtbFile(any()) + } else { + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(broadConsent) + ) + } + } + + @Test + fun shouldProcessDeleteRequest() { + mockMvc.delete("/mtbfile/TEST_12345678").andExpect { + status { + isAccepted() + } + } + + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(TtpConsentStatus.UNKNOWN_CHECK_FILE) + ) + verify(consentEvaluator, times(0)).check(any()) + } + } +} + +class Dnpm21MtbFile : ArgProvider( + // No Metadata and no broad consent => delete + Arguments.of( + buildMtb(null), + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, + "delete" + ), + // No Metadata and broad consent given => process + Arguments.of( + buildMtb(null), + TtpConsentStatus.BROAD_CONSENT_GIVEN, + "process" + ), + // No model project consent and no broad consent => delete + Arguments.of( + buildMtb(MvhMetadata.builder().modelProjectConsent(ModelProjectConsent.builder().build()).build()), + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, + "delete" + ), + // No model project consent and broad consent given => process + Arguments.of( + buildMtb(MvhMetadata.builder().modelProjectConsent(ModelProjectConsent.builder().build()).build()), + TtpConsentStatus.BROAD_CONSENT_GIVEN, + "process" + ), + // Model project consent given and no broad consent => process + Arguments.of( + buildMtb( + MvhMetadata.builder().modelProjectConsent( + ModelProjectConsent.builder().provisions( + listOf( + Provision.builder().date(Date()).type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) + ).build() + ).build() + ), + TtpConsentStatus.UNKNOWN_CHECK_FILE, + "process" + ), + // Model project consent given and broad consent given => process + Arguments.of( + buildMtb( + MvhMetadata.builder().modelProjectConsent( + ModelProjectConsent.builder().provisions( + listOf( + Provision.builder().date(Date()).type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + ) + ).build() + ).build() + ), + TtpConsentStatus.BROAD_CONSENT_GIVEN, + "process" + ) +) { + + companion object { + fun buildMtb(metadata: MvhMetadata?): Mtb { + return Mtb.builder() + .patient( + Patient.builder().id("TEST_12345678") + .birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))).gender( + GenderCoding.builder().code(GenderCodingCode.MALE).build() + ).build() + ) + .metadata(metadata) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder().id("1") + .patient(Reference.builder().id("TEST_12345678").build()) + .build() + ) + ) + .build() + } } } 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 58405cd..8460293 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt @@ -24,7 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper 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.ConsentByMtbFile +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.* @@ -95,7 +95,7 @@ class ExtensionsTest { gIcsConfigProperties, JacksonConfig().objectMapper(), FhirContext.forR4(), - ConsentByMtbFile() + MtbFileConsentService() ).embedBroadConsentResources(mtbFile, bundle) } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt index af93f7b..5a3fad0 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt @@ -7,7 +7,8 @@ import dev.dnpm.etl.processor.config.GIcsConfigProperties import dev.dnpm.etl.processor.config.JacksonConfig import dev.dnpm.etl.processor.consent.ConsentDomain import dev.dnpm.etl.processor.consent.GicsConsentService -import dev.pcvolkmer.mv64e.mtb.* +import dev.pcvolkmer.mv64e.mtb.Mtb +import dev.pcvolkmer.mv64e.mtb.Patient import org.assertj.core.api.Assertions.assertThat import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.CodeableConcept @@ -46,7 +47,7 @@ class ConsentProcessorTest { @Mock gicsConsentService: GicsConsentService, ) { - this.gIcsConfigProperties = GIcsConfigProperties(null, null, null) + this.gIcsConfigProperties = GIcsConfigProperties("https://gics.example.com") val jacksonConfig = JacksonConfig() this.objectMapper = jacksonConfig.objectMapper() this.fhirContext = JacksonConfig.fhirContext() @@ -67,10 +68,10 @@ class ConsentProcessorTest { assertThat(consentProcessor.toString()).isNotNull // prep gICS response doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService) - .getConsent(any(), any(), eq(ConsentDomain.BroadConsent)) + .getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT)) doAnswer { Bundle() }.whenever(gicsConsentService) - .getConsent(any(), any(), eq(ConsentDomain.Modelvorhaben64e)) + .getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E)) val inputMtb = Mtb.builder() .patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build()