1
0
mirror of https://github.com/pcvolkmer/mv64e-etl-processor synced 2025-09-13 09:02:50 +00:00

feat: check consent for DNPM 2.1 requests (#126)

Co-authored-by: Jakub Lidke <jakub.lidke@uni-marburg.de>
This commit is contained in:
2025-08-15 12:37:42 +02:00
committed by GitHub
parent be513f305a
commit 3eb1c79cec
21 changed files with 934 additions and 286 deletions

View File

@@ -102,6 +102,18 @@ class EtlProcessorApplicationTests : AbstractTestcontainerTest() {
.id("TEST_12345678") .id("TEST_12345678")
.build() .build()
) )
.metadata(
MvhMetadata
.builder()
.modelProjectConsent(
ModelProjectConsent
.builder()
.provisions(
listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build())
).build()
)
.build()
)
.diagnoses( .diagnoses(
listOf( listOf(
MtbDiagnosis.builder() MtbDiagnosis.builder()

View File

@@ -20,8 +20,9 @@
package dev.dnpm.etl.processor.config package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper 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.GicsConsentService
import dev.dnpm.etl.processor.consent.MtbFileConsentService
import dev.dnpm.etl.processor.input.KafkaInputListener import dev.dnpm.etl.processor.input.KafkaInputListener
import dev.dnpm.etl.processor.monitoring.RequestRepository import dev.dnpm.etl.processor.monitoring.RequestRepository
import dev.dnpm.etl.processor.output.KafkaMtbFileSender import dev.dnpm.etl.processor.output.KafkaMtbFileSender
@@ -53,7 +54,8 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean
AppSecurityConfiguration::class, AppSecurityConfiguration::class,
KafkaAutoConfiguration::class, KafkaAutoConfiguration::class,
AppKafkaConfiguration::class, AppKafkaConfiguration::class,
AppRestConfiguration::class AppRestConfiguration::class,
ConsentEvaluator::class
] ]
) )
@MockitoBean(types = [ObjectMapper::class]) @MockitoBean(types = [ObjectMapper::class])
@@ -281,7 +283,8 @@ class AppConfigurationTest {
@Nested @Nested
@TestPropertySource( @TestPropertySource(
properties = [ properties = [
"app.consent.service=GICS" "app.consent.service=GICS",
"app.consent.gics.uri=http://localhost:9000",
] ]
) )
inner class AppConfigurationConsentGicsTest(private val context: ApplicationContext) { 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 @Nested
inner class AppConfigurationConsentBuildinTest(private val context: ApplicationContext) { inner class AppConfigurationConsentBuildinTest(private val context: ApplicationContext) {
@Test @Test
fun shouldUseConfiguredGenerator() { fun shouldUseConfiguredGenerator() {
assertThat(context.getBean(ConsentByMtbFile::class.java)).isNotNull assertThat(context.getBean(MtbFileConsentService::class.java)).isNotNull
} }
} }

View File

@@ -20,11 +20,11 @@
package dev.dnpm.etl.processor.input package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.anyValueClass
import dev.dnpm.etl.processor.config.AppSecurityConfiguration 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.TtpConsentStatus
import dev.dnpm.etl.processor.consent.IGetConsent
import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.TokenRepository
import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.security.UserRoleRepository
import dev.dnpm.etl.processor.services.RequestProcessor import dev.dnpm.etl.processor.services.RequestProcessor
@@ -57,32 +57,37 @@ import java.util.*
classes = [ classes = [
MtbFileRestController::class, MtbFileRestController::class,
AppSecurityConfiguration::class, AppSecurityConfiguration::class,
ConsentByMtbFile::class, IGetConsent::class MtbFileConsentService::class
] ]
) )
@MockitoBean(types = [TokenRepository::class, RequestProcessor::class]) @MockitoBean(types = [TokenRepository::class, RequestProcessor::class, ConsentEvaluator::class])
@TestPropertySource( @TestPropertySource(
properties = [ properties = [
"app.pseudonymize.generator=BUILDIN", "app.pseudonymize.generator=BUILDIN",
"app.security.admin-user=admin", "app.security.admin-user=admin",
"app.security.admin-password={noop}very-secret", "app.security.admin-password={noop}very-secret",
"app.security.enable-tokens=true", "app.security.enable-tokens=true"
"app.consent.gics.enabled=false"
] ]
) )
class MtbFileRestControllerTest { class MtbFileRestControllerTest {
private lateinit var mockMvc: MockMvc lateinit var mockMvc: MockMvc
lateinit var requestProcessor: RequestProcessor
private lateinit var requestProcessor: RequestProcessor lateinit var consentEvaluator: ConsentEvaluator
@BeforeEach @BeforeEach
fun setup( fun setup(
@Autowired mockMvc: MockMvc, @Autowired mockMvc: MockMvc,
@Autowired requestProcessor: RequestProcessor @Autowired requestProcessor: RequestProcessor,
@Autowired consentEvaluator: ConsentEvaluator
) { ) {
this.mockMvc = mockMvc this.mockMvc = mockMvc
this.requestProcessor = requestProcessor this.requestProcessor = requestProcessor
this.consentEvaluator = consentEvaluator
doAnswer {
ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true)
}.whenever(consentEvaluator).check(any())
} }
@Test @Test
@@ -167,8 +172,7 @@ class MtbFileRestControllerTest {
"app.security.admin-user=admin", "app.security.admin-user=admin",
"app.security.admin-password={noop}very-secret", "app.security.admin-password={noop}very-secret",
"app.security.enable-tokens=true", "app.security.enable-tokens=true",
"app.security.enable-oidc=true", "app.security.enable-oidc=true"
"app.consent.gics.enabled=false"
] ]
) )
inner class WithOidcEnabled { inner class WithOidcEnabled {

View File

@@ -4,10 +4,10 @@ public enum ConsentDomain {
/** /**
* MII Broad consent * MII Broad consent
*/ */
BroadConsent, BROAD_CONSENT,
/** /**
* GenomDe Modelvohaben §64e * GenomDe Modellvorhaben §64e
*/ */
Modelvorhaben64e MODELLVORHABEN_64E
} }

View File

@@ -4,17 +4,9 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GIcsConfigProperties; import dev.dnpm.etl.processor.config.GIcsConfigProperties;
import java.util.Date;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.*;
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.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -22,109 +14,149 @@ import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.TerminatedRetryException; import org.springframework.retry.TerminatedRetryException;
import org.springframework.retry.support.RetryTemplate; import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder; 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); private final Logger log = LoggerFactory.getLogger(GicsConsentService.class);
public static final String IS_CONSENTED_ENDPOINT = "/$isConsented"; public static final String IS_CONSENTED_ENDPOINT = "/$isConsented";
public static final String IS_POLICY_STATES_FOR_PERSON_ENDPOINT = "/$currentPolicyStatesForPerson"; public static final String IS_POLICY_STATES_FOR_PERSON_ENDPOINT = "/$currentPolicyStatesForPerson";
private final RetryTemplate retryTemplate; private final RetryTemplate retryTemplate;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final FhirContext fhirContext; private final FhirContext fhirContext;
private final HttpHeaders httpHeader;
private final GIcsConfigProperties gIcsConfigProperties; 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.retryTemplate = retryTemplate;
this.restTemplate = restTemplate; this.restTemplate = restTemplate;
this.fhirContext = appFhirConfig.fhirContext(); this.fhirContext = appFhirConfig.fhirContext();
httpHeader = buildHeader(gIcsConfigProperties.getUsername(),
gIcsConfigProperties.getPassword());
this.gIcsConfigProperties = gIcsConfigProperties; this.gIcsConfigProperties = gIcsConfigProperties;
log.info("GicsConsentService initialized..."); log.info("GicsConsentService initialized...");
} }
public String getGicsUri(String endpoint) { protected Parameters getFhirRequestParameters(
if (url == null) { String personIdentifierValue
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) {
var result = new Parameters(); var result = new Parameters();
result.addParameter(new ParametersParameterComponent().setName("personIdentifier").setValue( result.addParameter(
new Identifier().setValue(personIdentifierValue) new ParametersParameterComponent()
.setSystem(configProperties.getPersonIdentifierSystem()))); .setName("personIdentifier")
result.addParameter(new ParametersParameterComponent().setName("domain") .setValue(
.setValue(new StringType().setValue(configProperties.getBroadConsentDomainName()))); new Identifier()
result.addParameter(new ParametersParameterComponent().setName("policy").setValue( .setValue(personIdentifierValue)
new Coding().setCode(configProperties.getBroadConsentPolicyCode()) .setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())
.setSystem(configProperties.getBroadConsentPolicySystem()))); )
);
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 * is mandatory parameter, but we ignore it via additional configuration parameter
* 'ignoreVersionNumber'. * 'ignoreVersionNumber'.
*/ */
result.addParameter(new ParametersParameterComponent().setName("version") result.addParameter(
.setValue(new StringType().setValue("1.1"))); new ParametersParameterComponent()
.setName("version")
.setValue(new StringType().setValue("1.1")
)
);
/* add config parameter with: /* add config parameter with:
* ignoreVersionNumber -> true ->> Reason is we cannot know which policy version each patient * 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. * has possibly signed or not, therefore we are happy with any version found.
* unknownStateIsConsideredAsDecline -> true * unknownStateIsConsideredAsDecline -> true
*/ */
var config = new ParametersParameterComponent().setName("config").addPart( var config = new ParametersParameterComponent()
new ParametersParameterComponent().setName("ignoreVersionNumber") .setName("config")
.setValue(new BooleanType().setValue(true))).addPart( .addPart(
new ParametersParameterComponent().setName("unknownStateIsConsideredAsDecline") new ParametersParameterComponent()
.setValue(new BooleanType().setValue(false))); .setName("ignoreVersionNumber")
.setValue(new BooleanType().setValue(true))
)
.addPart(
new ParametersParameterComponent()
.setName("unknownStateIsConsideredAsDecline")
.setValue(new BooleanType().setValue(false))
);
result.addParameter(config); result.addParameter(config);
return result; 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) { protected String callGicsApi(Parameters parameter, String endpoint) {
var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter);
HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.headersWithHttpBasicAuth());
HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader);
ResponseEntity<String> responseEntity;
try { try {
var url = getGicsUri(endpoint); var responseEntity = retryTemplate.execute(
ctx -> restTemplate.exchange(endpointUri(endpoint), HttpMethod.POST, requestEntity, String.class)
);
responseEntity = retryTemplate.execute( if (responseEntity.getStatusCode().is2xxSuccessful()) {
ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class)); 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) { } catch (RestClientException e) {
var msg = String.format("Get consents status request failed reason: '%s", var msg = String.format("Get consents status request failed reason: '%s",
e.getMessage()); e.getMessage());
@@ -137,36 +169,29 @@ public class GicsConsentService implements IGetConsent {
terminatedRetryException.getMessage()); terminatedRetryException.getMessage());
log.error(msg); log.error(msg);
return null; 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 @Override
public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) { public TtpConsentStatus getTtpBroadConsentStatus(String personIdentifierValue) {
var parameter = GicsConsentService.getIsConsentedRequestParam(gIcsConfigProperties, var consentStatusResponse = callGicsApi(
personIdentifierValue); getFhirRequestParameters(personIdentifierValue),
GicsConsentService.IS_CONSENTED_ENDPOINT
var consentStatusResponse = callGicsApi(parameter, );
GicsConsentService.IS_CONSENTED_ENDPOINT);
return evaluateConsentResponse(consentStatusResponse); return evaluateConsentResponse(consentStatusResponse);
} }
protected Bundle currentConsentForPersonAndTemplate(String personIdentifierValue, protected Bundle currentConsentForPersonAndTemplate(
ConsentDomain targetConsentDomain, Date requestDate) { String personIdentifierValue,
ConsentDomain consentDomain,
Date requestDate
) {
String consentDomain = getConsentDomain(targetConsentDomain); var requestParameter = buildRequestParameterCurrentPolicyStatesForPerson(
personIdentifierValue,
var requestParameter = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( requestDate,
gIcsConfigProperties, personIdentifierValue, requestDate, consentDomain); consentDomain
);
var consentDataSerialized = callGicsApi(requestParameter, var consentDataSerialized = callGicsApi(requestParameter,
GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT); GicsConsentService.IS_POLICY_STATES_FOR_PERSON_ENDPOINT);
@@ -184,8 +209,8 @@ public class GicsConsentService implements IGetConsent {
"Consent request failed! Check outcome:\n " + consentDataSerialized; "Consent request failed! Check outcome:\n " + consentDataSerialized;
log.error(errorMessage); log.error(errorMessage);
throw new IllegalStateException(errorMessage); throw new IllegalStateException(errorMessage);
} else if (iBaseResource instanceof Bundle) { } else if (iBaseResource instanceof Bundle bundle) {
return (Bundle) iBaseResource; return bundle;
} else { } else {
String errorMessage = "Consent request failed! Unexpected response received! -> " String errorMessage = "Consent request failed! Unexpected response received! -> "
+ consentDataSerialized; + consentDataSerialized;
@@ -195,40 +220,52 @@ public class GicsConsentService implements IGetConsent {
} }
@NotNull @NotNull
private String getConsentDomain(ConsentDomain targetConsentDomain) { private String getConsentDomainName(ConsentDomain targetConsentDomain) {
String consentDomain; return switch (targetConsentDomain) {
switch (targetConsentDomain) { case BROAD_CONSENT -> gIcsConfigProperties.getBroadConsentDomainName();
case BroadConsent -> consentDomain = gIcsConfigProperties.getBroadConsentDomainName(); case MODELLVORHABEN_64E -> gIcsConfigProperties.getGenomDeConsentDomainName();
case Modelvorhaben64e -> };
consentDomain = gIcsConfigProperties.getGenomDeConsentDomainName();
default -> throw new IllegalArgumentException(
"target ConsentDomain is missing but must be provided!");
}
return consentDomain;
} }
protected static Parameters buildRequestParameterCurrentPolicyStatesForPerson( protected Parameters buildRequestParameterCurrentPolicyStatesForPerson(
GIcsConfigProperties gIcsConfigProperties, String personIdentifierValue, Date requestDate, String personIdentifierValue,
String targetDomain) { Date requestDate,
ConsentDomain consentDomain
) {
var requestParameter = new Parameters(); var requestParameter = new Parameters();
requestParameter.addParameter(new ParametersParameterComponent().setName("personIdentifier") requestParameter.addParameter(
.setValue(new Identifier().setValue(personIdentifierValue) new ParametersParameterComponent()
.setSystem(gIcsConfigProperties.getPersonIdentifierSystem()))); .setName("personIdentifier")
.setValue(
new Identifier()
.setValue(personIdentifierValue)
.setSystem(this.gIcsConfigProperties.getPersonIdentifierSystem())
)
);
requestParameter.addParameter(new ParametersParameterComponent().setName("domain") requestParameter.addParameter(
.setValue(new StringType().setValue(targetDomain))); new ParametersParameterComponent()
.setName("domain")
.setValue(new StringType().setValue(getConsentDomainName(consentDomain)))
);
Parameters nestedConfigParameters = new Parameters(); Parameters nestedConfigParameters = new Parameters();
nestedConfigParameters.addParameter( nestedConfigParameters
new ParametersParameterComponent().setName("idMatchingType").setValue( .addParameter(
new Coding().setSystem( new ParametersParameterComponent()
"https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType") .setName("idMatchingType")
.setCode("AT_LEAST_ONE"))).addParameter("ignoreVersionNumber", false) .setValue(new Coding()
.setSystem("https://ths-greifswald.de/fhir/CodeSystem/gics/IdMatchingType")
.setCode("AT_LEAST_ONE")
)
)
.addParameter("ignoreVersionNumber", false)
.addParameter("unknownStateIsConsideredAsDecline", false) .addParameter("unknownStateIsConsideredAsDecline", false)
.addParameter("requestDate", new DateType().setValue(requestDate)); .addParameter("requestDate", new DateType().setValue(requestDate));
requestParameter.addParameter(new ParametersParameterComponent().setName("config").addPart() requestParameter.addParameter(
.setResource(nestedConfigParameters)); new ParametersParameterComponent().setName("config").addPart().setResource(nestedConfigParameters)
);
return requestParameter; return requestParameter;
} }
@@ -265,17 +302,6 @@ public class GicsConsentService implements IGetConsent {
@Override @Override
public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) { public Bundle getConsent(String patientId, Date requestDate, ConsentDomain consentDomain) {
switch (consentDomain) { return currentConsentForPersonAndTemplate(patientId, consentDomain, requestDate);
case BroadConsent -> {
return currentConsentForPersonAndTemplate(patientId, ConsentDomain.BroadConsent,
requestDate);
}
case Modelvorhaben64e -> {
return currentConsentForPersonAndTemplate(patientId,
ConsentDomain.Modelvorhaben64e, requestDate);
}
}
return new Bundle();
} }
} }

View File

@@ -3,7 +3,7 @@ package dev.dnpm.etl.processor.consent;
import java.util.Date; import java.util.Date;
import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle;
public interface IGetConsent { public interface IConsentService {
/** /**
* Get broad consent status for a patient identifier * Get broad consent status for a patient identifier

View File

@@ -5,11 +5,11 @@ import org.hl7.fhir.r4.model.Bundle;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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..."); log.info("ConsentCheckFileBased initialized...");
} }

View File

@@ -73,8 +73,8 @@ data class GIcsConfigProperties(
* *
*/ */
val uri: String?, val uri: String?,
val username: String?, val username: String? = null,
val password: String?, val password: String? = null,
/** /**
* gICS specific system * gICS specific system

View File

@@ -20,9 +20,9 @@
package dev.dnpm.etl.processor.config package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper 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.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.monitoring.*
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.Generator import dev.dnpm.etl.processor.pseudonym.Generator
@@ -218,7 +218,7 @@ class AppConfiguration {
retryTemplate: RetryTemplate, retryTemplate: RetryTemplate,
restTemplate: RestTemplate, restTemplate: RestTemplate,
appFhirConfig: AppFhirConfig appFhirConfig: AppFhirConfig
): IGetConsent { ): IConsentService {
return GicsConsentService( return GicsConsentService(
gIcsConfigProperties, gIcsConfigProperties,
retryTemplate, retryTemplate,
@@ -234,7 +234,7 @@ class AppConfiguration {
gIcsConfigProperties: GIcsConfigProperties, gIcsConfigProperties: GIcsConfigProperties,
getObjectMapper: ObjectMapper, getObjectMapper: ObjectMapper,
appFhirConfig: AppFhirConfig, appFhirConfig: AppFhirConfig,
gicsConsentService: IGetConsent gicsConsentService: IConsentService
): ConsentProcessor { ): ConsentProcessor {
return ConsentProcessor( return ConsentProcessor(
configProperties, configProperties,
@@ -261,8 +261,8 @@ class AppConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
fun iGetConsentService(): IGetConsent { fun iGetConsentService(): IConsentService {
return ConsentByMtbFile() return MtbFileConsentService()
} }
} }
@@ -271,13 +271,9 @@ class GicsEnabledCondition :
AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) { AnyNestedCondition(ConfigurationCondition.ConfigurationPhase.REGISTER_BEAN) {
@ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics") @ConditionalOnProperty(name = ["app.consent.service"], havingValue = "gics")
@ConditionalOnProperty(name = ["app.consent.gics.uri"])
class OnGicsServiceSelected { class OnGicsServiceSelected {
// Just for Condition // Just for Condition
} }
@ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true")
class OnGicsEnabled {
// Just for Condition
}
} }

View File

@@ -20,6 +20,7 @@
package dev.dnpm.etl.processor.config package dev.dnpm.etl.processor.config
import com.fasterxml.jackson.databind.ObjectMapper 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.input.KafkaInputListener
import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
@@ -100,9 +101,10 @@ class AppKafkaConfiguration {
@ConditionalOnProperty(value = ["app.kafka.input-topic"]) @ConditionalOnProperty(value = ["app.kafka.input-topic"])
fun kafkaInputListener( fun kafkaInputListener(
requestProcessor: RequestProcessor, requestProcessor: RequestProcessor,
objectMapper: ObjectMapper objectMapper: ObjectMapper,
consentEvaluator: ConsentEvaluator
): KafkaInputListener { ): KafkaInputListener {
return KafkaInputListener(requestProcessor, objectMapper) return KafkaInputListener(requestProcessor, consentEvaluator, objectMapper)
} }
@Bean @Bean

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View File

@@ -23,9 +23,9 @@ import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.PatientId
import dev.dnpm.etl.processor.RequestId 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.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor import dev.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.ConsentProvision
import dev.pcvolkmer.mv64e.mtb.Mtb import dev.pcvolkmer.mv64e.mtb.Mtb
import org.apache.kafka.clients.consumer.ConsumerRecord import org.apache.kafka.clients.consumer.ConsumerRecord
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -34,6 +34,7 @@ import org.springframework.kafka.listener.MessageListener
class KafkaInputListener( class KafkaInputListener(
private val requestProcessor: RequestProcessor, private val requestProcessor: RequestProcessor,
private val consentEvaluator: ConsentEvaluator,
private val objectMapper: ObjectMapper private val objectMapper: ObjectMapper
) : MessageListener<String, String> { ) : MessageListener<String, String> {
private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java) private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java)
@@ -70,8 +71,7 @@ class KafkaInputListener(
RequestId("") RequestId("")
} }
// TODO: Use MV Consent for now - needs to be replaced with proper consent evaluation if (consentEvaluator.check(mtbFile).hasConsent()) {
if (mtbFile.metadata.modelProjectConsent.provisions.filter { it.type == ConsentProvision.PERMIT }.isNotEmpty()) {
logger.debug("Accepted MTB File for processing") logger.debug("Accepted MTB File for processing")
if (requestId.isBlank()) { if (requestId.isBlank()) {
requestProcessor.processMtbFile(mtbFile) requestProcessor.processMtbFile(mtbFile)

View File

@@ -21,7 +21,7 @@ package dev.dnpm.etl.processor.input
import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.PatientId 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.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor import dev.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.Mtb import dev.pcvolkmer.mv64e.mtb.Mtb
@@ -34,9 +34,8 @@ import org.springframework.web.bind.annotation.*
@RequestMapping(path = ["mtbfile", "mtb"]) @RequestMapping(path = ["mtbfile", "mtb"])
class MtbFileRestController( class MtbFileRestController(
private val requestProcessor: RequestProcessor, private val requestProcessor: RequestProcessor,
private val iGetConsent: IGetConsent private val consentEvaluator: ConsentEvaluator
) { ) {
private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java)
@GetMapping @GetMapping
@@ -46,8 +45,15 @@ class MtbFileRestController(
@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE]) @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE])
fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> { fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> {
val consentEvaluation = consentEvaluator.check(mtbFile)
if (consentEvaluation.hasConsent()) {
logger.debug("Accepted MTB File (DNPM V2) for processing") logger.debug("Accepted MTB File (DNPM V2) for processing")
requestProcessor.processMtbFile(mtbFile) 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() return ResponseEntity.accepted().build()
} }

View File

@@ -6,9 +6,9 @@ import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.config.GIcsConfigProperties 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.ConsentDomain
import dev.dnpm.etl.processor.consent.IGetConsent import dev.dnpm.etl.processor.consent.IConsentService
import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized
import dev.pcvolkmer.mv64e.mtb.* import dev.pcvolkmer.mv64e.mtb.*
import org.apache.commons.lang3.NotImplementedException import org.apache.commons.lang3.NotImplementedException
@@ -31,7 +31,7 @@ class ConsentProcessor(
private val gIcsConfigProperties: GIcsConfigProperties, private val gIcsConfigProperties: GIcsConfigProperties,
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val fhirContext: FhirContext, private val fhirContext: FhirContext,
private val consentService: IGetConsent private val consentService: IConsentService
) { ) {
private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor") private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor")
@@ -49,7 +49,7 @@ class ConsentProcessor(
* *
*/ */
fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean {
if (consentService is ConsentByMtbFile) { if (consentService is MtbFileConsentService) {
// consent check is disabled // consent check is disabled
return true return true
} }
@@ -70,7 +70,7 @@ class ConsentProcessor(
* broad consent * broad consent
*/ */
val broadConsent = consentService.getConsent( val broadConsent = consentService.getConsent(
personIdentifierValue, requestDate, ConsentDomain.BroadConsent personIdentifierValue, requestDate, ConsentDomain.BROAD_CONSENT
) )
val broadConsentHasBeenAsked = !broadConsent.entry.isEmpty() val broadConsentHasBeenAsked = !broadConsent.entry.isEmpty()
@@ -78,7 +78,7 @@ class ConsentProcessor(
if (!broadConsentHasBeenAsked) return false if (!broadConsentHasBeenAsked) return false
val genomeDeConsent = consentService.getConsent( val genomeDeConsent = consentService.getConsent(
personIdentifierValue, requestDate, ConsentDomain.Modelvorhaben64e personIdentifierValue, requestDate, ConsentDomain.MODELLVORHABEN_64E
) )
addGenomeDbProvisions(mtbFile, genomeDeConsent) addGenomeDbProvisions(mtbFile, genomeDeConsent)
@@ -88,11 +88,11 @@ class ConsentProcessor(
embedBroadConsentResources(mtbFile, broadConsent) embedBroadConsentResources(mtbFile, broadConsent)
val broadConsentStatus = getProvisionTypeByPolicyCode( val broadConsentStatus = getProvisionTypeByPolicyCode(
broadConsent, requestDate, ConsentDomain.BroadConsent broadConsent, requestDate, ConsentDomain.BROAD_CONSENT
) )
val genomDeSequencingStatus = getProvisionTypeByPolicyCode( val genomDeSequencingStatus = getProvisionTypeByPolicyCode(
genomeDeConsent, requestDate, ConsentDomain.Modelvorhaben64e genomeDeConsent, requestDate, ConsentDomain.MODELLVORHABEN_64E
) )
if (Consent.ConsentProvisionType.NULL == broadConsentStatus) { if (Consent.ConsentProvisionType.NULL == broadConsentStatus) {
@@ -204,10 +204,10 @@ class ConsentProcessor(
): Consent.ConsentProvisionType { ): Consent.ConsentProvisionType {
val code: String? val code: String?
val system: String? val system: String?
if (ConsentDomain.BroadConsent == consentDomain) { if (ConsentDomain.BROAD_CONSENT == consentDomain) {
code = gIcsConfigProperties.broadConsentPolicyCode code = gIcsConfigProperties.broadConsentPolicyCode
system = gIcsConfigProperties.broadConsentPolicySystem system = gIcsConfigProperties.broadConsentPolicySystem
} else if (ConsentDomain.Modelvorhaben64e == consentDomain) { } else if (ConsentDomain.MODELLVORHABEN_64E == consentDomain) {
code = gIcsConfigProperties.genomeDePolicyCode code = gIcsConfigProperties.genomeDePolicyCode
system = gIcsConfigProperties.genomeDePolicySystem system = gIcsConfigProperties.genomeDePolicySystem
} else { } else {

View File

@@ -1,85 +1,112 @@
package dev.dnpm.etl.processor.consent; 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 com.fasterxml.jackson.databind.ObjectMapper;
import dev.dnpm.etl.processor.config.AppConfiguration; import dev.dnpm.etl.processor.config.AppConfiguration;
import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.AppFhirConfig;
import dev.dnpm.etl.processor.config.GIcsConfigProperties; import dev.dnpm.etl.processor.config.GIcsConfigProperties;
import java.time.Instant; import org.hl7.fhir.r4.model.*;
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.OperationOutcome.IssueSeverity; import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.r4.model.OperationOutcome.IssueType; import org.hl7.fhir.r4.model.OperationOutcome.IssueType;
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; 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.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.client.MockRestServiceServer; 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}) @ContextConfiguration(classes = {AppConfiguration.class, ObjectMapper.class})
@TestPropertySource(properties = {"app.consent.gics.enabled=true", @TestPropertySource(properties = {
"app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"}) "app.consent.service=gics",
"app.consent.gics.uri=http://localhost:8090/ttp-fhir/fhir/gics"
})
@RestClientTest @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; MockRestServiceServer mockRestServiceServer;
@Autowired
GicsConsentService gicsConsentService;
@Autowired
AppConfiguration appConfiguration;
@Autowired
AppFhirConfig appFhirConfig; AppFhirConfig appFhirConfig;
@Autowired
GIcsConfigProperties gIcsConfigProperties; GIcsConfigProperties gIcsConfigProperties;
GicsConsentService gicsConsentService;
@BeforeEach @BeforeEach
public void setUp() { void setUp(
mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate()); @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 @Test
void getTtpBroadConsentStatus() { void shouldReturnTtpBroadConsentStatus() {
final Parameters responseConsented = new Parameters().addParameter( final Parameters consentedResponse = new Parameters()
new ParametersParameterComponent().setName("consented") .addParameter(
.setValue(new BooleanType().setValue(true))); new ParametersParameterComponent()
.setName("consented")
.setValue(new BooleanType().setValue(true))
);
mockRestServiceServer.expect(requestTo( mockRestServiceServer
"http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)) .expect(
.andRespond(withSuccess(appFhirConfig.fhirContext().newJsonParser() requestTo(
.encodeResourceToString(responseConsented), MediaType.APPLICATION_JSON)); "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"); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN); assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_GIVEN);
} }
@Test @Test
void consentRevoced() { void shouldReturnRevokedConsent() {
final Parameters responseRevoced = new Parameters().addParameter( final Parameters revokedResponse = new Parameters()
new ParametersParameterComponent().setName("consented") .addParameter(
.setValue(new BooleanType().setValue(false))); new ParametersParameterComponent()
.setName("consented")
.setValue(new BooleanType().setValue(false))
);
mockRestServiceServer.expect(requestTo( mockRestServiceServer
"http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)) .expect(
.andRespond(withSuccess( requestTo(
appFhirConfig.fhirContext().newJsonParser().encodeResourceToString(responseRevoced), "http://localhost:8090/ttp-fhir/fhir/gics" + GicsConsentService.IS_CONSENTED_ENDPOINT)
MediaType.APPLICATION_JSON)); )
.andRespond(
withSuccess(
appFhirConfig.fhirContext().newJsonParser().encodeResourceToString(revokedResponse),
MediaType.APPLICATION_JSON)
);
var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456"); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED); assertThat(consentStatus).isEqualTo(TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED);
@@ -87,15 +114,39 @@ public class GicsConsentServiceTest {
@Test @Test
void gicsParameterInvalid() { void shouldReturnInvalidParameterResponse() {
final OperationOutcome responseErrorOutcome = new OperationOutcome().addIssue( final OperationOutcome responseWithErrorOutcome = new OperationOutcome()
new OperationOutcomeIssueComponent().setSeverity(IssueSeverity.ERROR) .addIssue(
.setCode(IssueType.PROCESSING).setDiagnostics("Invalid policy parameter...")); new OperationOutcomeIssueComponent()
.setSeverity(IssueSeverity.ERROR)
.setCode(IssueType.PROCESSING)
.setDiagnostics("Invalid policy parameter...")
);
mockRestServiceServer.expect( mockRestServiceServer
requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)).andRespond( .expect(
withSuccess(appFhirConfig.fhirContext().newJsonParser() requestTo(GICS_BASE_URI + GicsConsentService.IS_CONSENTED_ENDPOINT)
.encodeResourceToString(responseErrorOutcome), MediaType.APPLICATION_JSON)); )
.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"); var consentStatus = gicsConsentService.getTtpBroadConsentStatus("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK); assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
@@ -103,20 +154,27 @@ public class GicsConsentServiceTest {
@Test @Test
void buildRequestParameterCurrentPolicyStatesForPersonTest() { void buildRequestParameterCurrentPolicyStatesForPersonTest() {
String pid = "12345678"; String pid = "12345678";
var result = GicsConsentService.buildRequestParameterCurrentPolicyStatesForPerson( var result = gicsConsentService
gIcsConfigProperties, pid, Date.from(Instant.now()), .buildRequestParameterCurrentPolicyStatesForPerson(
gIcsConfigProperties.getGenomDeConsentDomainName()); pid,
Date.from(Instant.now()),
ConsentDomain.MODELLVORHABEN_64E
);
assertThat(result.getParameter().size()).as("should contain 3 parameter resources") assertThat(result.getParameter())
.isEqualTo(3); .as("should contain 3 parameter resources")
.hasSize(3);
assertThat(((StringType) result.getParameter("domain").getValue()).getValue()).isEqualTo( assertThat(((StringType) result.getParameter("domain").getValue()).getValue())
gIcsConfigProperties.getGenomDeConsentDomainName()); .isEqualTo(
assertThat( gIcsConfigProperties.getGenomDeConsentDomainName()
((Identifier) result.getParameter("personIdentifier").getValue()).getValue()).isEqualTo( );
pid);
assertThat(((Identifier) result.getParameter("personIdentifier").getValue()).getValue())
.isEqualTo(
pid
);
} }

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}
}

View File

@@ -18,3 +18,14 @@
*/ */
package dev.dnpm.etl.processor 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<out Arguments> = Stream.of(*data)
}

View File

@@ -20,8 +20,10 @@
package dev.dnpm.etl.processor.input package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.CustomMediaType 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.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.* import dev.pcvolkmer.mv64e.mtb.*
import org.apache.kafka.clients.consumer.ConsumerRecord import org.apache.kafka.clients.consumer.ConsumerRecord
@@ -40,21 +42,32 @@ import java.util.*
class KafkaInputListenerTest { class KafkaInputListenerTest {
private lateinit var requestProcessor: RequestProcessor private lateinit var requestProcessor: RequestProcessor
private lateinit var consentEvaluator: ConsentEvaluator
private lateinit var objectMapper: ObjectMapper private lateinit var objectMapper: ObjectMapper
private lateinit var kafkaInputListener: KafkaInputListener private lateinit var kafkaInputListener: KafkaInputListener
@BeforeEach @BeforeEach
fun setup( fun setup(
@Mock requestProcessor: RequestProcessor, @Mock requestProcessor: RequestProcessor,
@Mock consentEvaluator: ConsentEvaluator,
) { ) {
this.requestProcessor = requestProcessor this.requestProcessor = requestProcessor
this.consentEvaluator = consentEvaluator
this.objectMapper = ObjectMapper() this.objectMapper = ObjectMapper()
this.kafkaInputListener = KafkaInputListener(requestProcessor, objectMapper) this.kafkaInputListener = KafkaInputListener(requestProcessor, consentEvaluator, objectMapper)
} }
@Test @Test
fun shouldProcessMtbFileRequest() { fun shouldProcessMtbFileRequest() {
whenever(consentEvaluator.check(any())).thenReturn(
ConsentEvaluation(
TtpConsentStatus.BROAD_CONSENT_GIVEN,
true
)
)
val mtbFile = Mtb.builder() val mtbFile = Mtb.builder()
.patient(Patient.builder().id("DUMMY_12345678").build()) .patient(Patient.builder().id("DUMMY_12345678").build())
.metadata( .metadata(
@@ -64,7 +77,10 @@ class KafkaInputListenerTest {
ModelProjectConsent ModelProjectConsent
.builder() .builder()
.provisions( .provisions(
listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) listOf(
Provision.builder().type(ConsentProvision.PERMIT)
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
)
).build() ).build()
) )
.build() .build()
@@ -86,6 +102,13 @@ class KafkaInputListenerTest {
@Test @Test
fun shouldProcessDeleteRequest() { fun shouldProcessDeleteRequest() {
whenever(consentEvaluator.check(any())).thenReturn(
ConsentEvaluation(
TtpConsentStatus.BROAD_CONSENT_GIVEN,
false
)
)
val mtbFile = Mtb.builder() val mtbFile = Mtb.builder()
.patient(Patient.builder().id("DUMMY_12345678").build()) .patient(Patient.builder().id("DUMMY_12345678").build())
.metadata( .metadata(
@@ -95,7 +118,10 @@ class KafkaInputListenerTest {
ModelProjectConsent ModelProjectConsent
.builder() .builder()
.provisions( .provisions(
listOf(Provision.builder().type(ConsentProvision.DENY).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) listOf(
Provision.builder().type(ConsentProvision.DENY)
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
)
).build() ).build()
) )
.build() .build()
@@ -120,6 +146,13 @@ class KafkaInputListenerTest {
@Test @Test
fun shouldProcessMtbFileRequestWithExistingRequestId() { fun shouldProcessMtbFileRequestWithExistingRequestId() {
whenever(consentEvaluator.check(any())).thenReturn(
ConsentEvaluation(
TtpConsentStatus.BROAD_CONSENT_GIVEN,
true
)
)
val mtbFile = Mtb.builder() val mtbFile = Mtb.builder()
.patient(Patient.builder().id("DUMMY_12345678").build()) .patient(Patient.builder().id("DUMMY_12345678").build())
.metadata( .metadata(
@@ -129,7 +162,10 @@ class KafkaInputListenerTest {
ModelProjectConsent ModelProjectConsent
.builder() .builder()
.provisions( .provisions(
listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) listOf(
Provision.builder().type(ConsentProvision.PERMIT)
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
)
).build() ).build()
) )
.build() .build()
@@ -158,6 +194,13 @@ class KafkaInputListenerTest {
@Test @Test
fun shouldProcessDeleteRequestWithExistingRequestId() { fun shouldProcessDeleteRequestWithExistingRequestId() {
whenever(consentEvaluator.check(any())).thenReturn(
ConsentEvaluation(
TtpConsentStatus.BROAD_CONSENT_GIVEN,
false
)
)
val mtbFile = Mtb.builder() val mtbFile = Mtb.builder()
.patient(Patient.builder().id("DUMMY_12345678").build()) .patient(Patient.builder().id("DUMMY_12345678").build())
.metadata( .metadata(
@@ -167,7 +210,10 @@ class KafkaInputListenerTest {
ModelProjectConsent ModelProjectConsent
.builder() .builder()
.provisions( .provisions(
listOf(Provision.builder().type(ConsentProvision.DENY).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) listOf(
Provision.builder().type(ConsentProvision.DENY)
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
)
).build() ).build()
) )
.build() .build()
@@ -208,7 +254,10 @@ class KafkaInputListenerTest {
ModelProjectConsent ModelProjectConsent
.builder() .builder()
.provisions( .provisions(
listOf(Provision.builder().type(ConsentProvision.DENY).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) listOf(
Provision.builder().type(ConsentProvision.DENY)
.purpose(ModelProjectConsentPurpose.SEQUENCING).build()
)
).build() ).build()
) )
.build() .build()

View File

@@ -20,23 +20,34 @@
package dev.dnpm.etl.processor.input package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.ArgProvider
import dev.dnpm.etl.processor.CustomMediaType 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.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.BeforeEach
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith 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.Mock
import org.mockito.Mockito.times import org.mockito.Mockito.times
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.anyValueClass
import org.mockito.kotlin.whenever
import org.springframework.core.io.ClassPathResource import org.springframework.core.io.ClassPathResource
import org.springframework.test.web.servlet.MockMvc 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.post
import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.test.web.servlet.setup.MockMvcBuilders
import java.time.Instant
import java.util.*
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
class MtbFileRestControllerTest { class MtbFileRestControllerTest {
@@ -49,22 +60,31 @@ class MtbFileRestControllerTest {
private lateinit var mockMvc: MockMvc private lateinit var mockMvc: MockMvc
private lateinit var requestProcessor: RequestProcessor private lateinit var requestProcessor: RequestProcessor
private lateinit var consentEvaluator: ConsentEvaluator
@BeforeEach @BeforeEach
fun setup( fun setup(
@Mock requestProcessor: RequestProcessor, @Mock requestProcessor: RequestProcessor,
@Mock gicsConsentService: GicsConsentService @Mock consentEvaluator: ConsentEvaluator
) { ) {
this.requestProcessor = requestProcessor this.requestProcessor = requestProcessor
this.consentEvaluator = consentEvaluator
val controller = MtbFileRestController( val controller = MtbFileRestController(
requestProcessor, requestProcessor,
gicsConsentService consentEvaluator
) )
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
} }
@Test @Test
fun shouldRespondPostRequest() { fun shouldRespondPostRequest() {
whenever(consentEvaluator.check(any())).thenReturn(
ConsentEvaluation(
TtpConsentStatus.BROAD_CONSENT_GIVEN,
true
)
)
val mtbFileContent = val mtbFileContent =
ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8) ClassPathResource("mv64e-mtb-fake-patient.json").inputStream.readAllBytes().toString(Charsets.UTF_8)
@@ -80,5 +100,127 @@ class MtbFileRestControllerTest {
verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) verify(requestProcessor, times(1)).processMtbFile(any<Mtb>())
} }
@ParameterizedTest
@ArgumentsSource(Dnpm21MtbFile::class)
fun shouldProcessPostRequest(mtb: Mtb, broadConsent: TtpConsentStatus, shouldProcess: String) {
whenever(consentEvaluator.check(any<Mtb>())).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<Mtb>())
} 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<Mtb>())
}
}
}
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()
}
} }
} }

View File

@@ -24,7 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import dev.dnpm.etl.processor.config.AppConfigProperties import dev.dnpm.etl.processor.config.AppConfigProperties
import dev.dnpm.etl.processor.config.GIcsConfigProperties import dev.dnpm.etl.processor.config.GIcsConfigProperties
import dev.dnpm.etl.processor.config.JacksonConfig 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.ConsentProcessor
import dev.dnpm.etl.processor.services.ConsentProcessorTest import dev.dnpm.etl.processor.services.ConsentProcessorTest
import dev.pcvolkmer.mv64e.mtb.* import dev.pcvolkmer.mv64e.mtb.*
@@ -95,7 +95,7 @@ class ExtensionsTest {
gIcsConfigProperties, gIcsConfigProperties,
JacksonConfig().objectMapper(), JacksonConfig().objectMapper(),
FhirContext.forR4(), FhirContext.forR4(),
ConsentByMtbFile() MtbFileConsentService()
).embedBroadConsentResources(mtbFile, bundle) ).embedBroadConsentResources(mtbFile, bundle)
} }

View File

@@ -7,7 +7,8 @@ import dev.dnpm.etl.processor.config.GIcsConfigProperties
import dev.dnpm.etl.processor.config.JacksonConfig import dev.dnpm.etl.processor.config.JacksonConfig
import dev.dnpm.etl.processor.consent.ConsentDomain import dev.dnpm.etl.processor.consent.ConsentDomain
import dev.dnpm.etl.processor.consent.GicsConsentService 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.assertj.core.api.Assertions.assertThat
import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.CodeableConcept
@@ -46,7 +47,7 @@ class ConsentProcessorTest {
@Mock gicsConsentService: GicsConsentService, @Mock gicsConsentService: GicsConsentService,
) { ) {
this.gIcsConfigProperties = GIcsConfigProperties(null, null, null) this.gIcsConfigProperties = GIcsConfigProperties("https://gics.example.com")
val jacksonConfig = JacksonConfig() val jacksonConfig = JacksonConfig()
this.objectMapper = jacksonConfig.objectMapper() this.objectMapper = jacksonConfig.objectMapper()
this.fhirContext = JacksonConfig.fhirContext() this.fhirContext = JacksonConfig.fhirContext()
@@ -67,10 +68,10 @@ class ConsentProcessorTest {
assertThat(consentProcessor.toString()).isNotNull assertThat(consentProcessor.toString()).isNotNull
// prep gICS response // prep gICS response
doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService) doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService)
.getConsent(any(), any(), eq(ConsentDomain.BroadConsent)) .getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT))
doAnswer { Bundle() }.whenever(gicsConsentService) doAnswer { Bundle() }.whenever(gicsConsentService)
.getConsent(any(), any(), eq(ConsentDomain.Modelvorhaben64e)) .getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E))
val inputMtb = Mtb.builder() val inputMtb = Mtb.builder()
.patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build() .patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build()