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:
@@ -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()
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -4,10 +4,10 @@ public enum ConsentDomain {
|
||||
/**
|
||||
* MII Broad consent
|
||||
*/
|
||||
BroadConsent,
|
||||
BROAD_CONSENT,
|
||||
|
||||
/**
|
||||
* GenomDe Modelvohaben §64e
|
||||
* GenomDe Modellvorhaben §64e
|
||||
*/
|
||||
Modelvorhaben64e
|
||||
MODELLVORHABEN_64E
|
||||
}
|
||||
|
@@ -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<String> requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader);
|
||||
ResponseEntity<String> responseEntity;
|
||||
HttpEntity<String> 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);
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
@@ -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...");
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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<String, String> {
|
||||
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)
|
||||
|
@@ -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<Unit> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -17,4 +17,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
}
|
||||
|
@@ -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()
|
||||
|
Reference in New Issue
Block a user