1
0
mirror of https://github.com/pcvolkmer/etl-processor.git synced 2025-07-01 14:12:55 +00:00

fix: * Status prüfung gIcs überarbeitet.* Tests für GicsConsentService implementiert. * Überschreiben des MTB-File Consent, falls gICS aktiviert ist

This commit is contained in:
Jakub Lidke
2025-05-05 12:28:54 +02:00
parent 2a28a4b3d4
commit 542dc61811
6 changed files with 256 additions and 85 deletions

View File

@ -59,40 +59,3 @@ services:
# POSTGRES_DB: dev
# POSTGRES_USER: dev
# POSTGRES_PASSWORD: dev
mysql:
image: mysql:8
container_name: gics-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
TZ: Europe/Berlin
ports:
- "3306:3306"
volumes:
- ./sqls:/docker-entrypoint-initdb.d
command: --max_allowed_packet=20M --default-time-zone=Europe/Berlin
gics:
image: registry.diz.uni-marburg.de/ths/gics:2023.1.3
container_name: gics-wildfly
restart: unless-stopped
ports:
- "8090:8080"
- "127.0.0.1:9992:9990"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- mysql
consent-data-loader:
image: confluentinc/cp-kafkacat:7.1.12
entrypoint: ["/bin/bash", "-c"]
command: >
"kafkacat -b kafka:19092 -K: -t consent-json -P -l /data/consent-data.ndjson"
volumes:
- ./consent-data.ndjson:/data/consent-data.ndjson:ro
depends_on:
kafka:
condition: service_healthy

View File

@ -1,12 +1,14 @@
package dev.dnpm.etl.processor.consent;
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 org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Coding;
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.StringType;
@ -31,7 +33,7 @@ public class GicsConsentService implements ICheckConsent {
private final GIcsConfigProperties gIcsConfigProperties;
public final String IS_CONSENTED_PATH = "/ttp-fhir/fhir/gics/$isConsented";
public static final String IS_CONSENTED_ENDPOINT = "/$isConsented";
private final RetryTemplate retryTemplate;
private final RestTemplate restTemplate;
private final FhirContext fhirContext;
@ -56,8 +58,7 @@ public class GicsConsentService implements ICheckConsent {
throw new IllegalArgumentException(
"gICS base URL is empty - should call gICS with false configuration.");
}
url = UriComponentsBuilder.fromHttpUrl(gIcsBaseUri)
.path(IS_CONSENTED_PATH)
url = UriComponentsBuilder.fromHttpUrl(gIcsBaseUri).path(IS_CONSENTED_ENDPOINT)
.toUriString();
}
return url;
@ -84,14 +85,20 @@ public class GicsConsentService implements ICheckConsent {
.setSystem(configProperties.getPersonIdentifierSystem())));
result.addParameter(new ParametersParameterComponent().setName("domain")
.setValue(new StringType().setValue(configProperties.getConsentDomainName())));
result.addParameter(new ParametersParameterComponent().setName("policy")
.setValue(new Coding().setCode(configProperties.getPolicyCode())
result.addParameter(new ParametersParameterComponent().setName("policy").setValue(
new Coding().setCode(configProperties.getPolicyCode())
.setSystem(configProperties.getPolicySystem())));
/*
* is mandatory parameter, but we ignore it via additional configuration parameter
* 'ignoreVersionNumber'.
*/
result.addParameter(new ParametersParameterComponent().setName("version")
.setValue(new StringType().setValue(configProperties.getParameterVersion())));
.setValue(new StringType().setValue("1.1")));
/* add config parameter with:
* ignoreVersionNumber -> true
* 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(
@ -110,9 +117,10 @@ public class GicsConsentService implements ICheckConsent {
HttpEntity<String> requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader);
ResponseEntity<String> responseEntity;
try {
var url = getGicsUri();
responseEntity = retryTemplate.execute(
ctx -> restTemplate.exchange(getGicsUri(), HttpMethod.POST, requestEntity,
String.class));
ctx -> restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class));
} catch (RestClientException e) {
var msg = String.format("Get consents status request failed reason: '%s",
e.getMessage());
@ -123,8 +131,7 @@ public class GicsConsentService implements ICheckConsent {
var msg = String.format(
"Get consents status process has been terminated. termination reason: '%s",
terminatedRetryException.getMessage());
log.error(msg
);
log.error(msg);
return null;
}
@ -153,18 +160,31 @@ public class GicsConsentService implements ICheckConsent {
if (consentStatusResponse == null) {
return TtpConsentStatus.FAILED_TO_ASK;
}
var responseParameters = fhirContext.newJsonParser()
.parseResource(Parameters.class, consentStatusResponse);
try {
var response = fhirContext.newJsonParser().parseResource(consentStatusResponse);
var responseValue = responseParameters.getParameter("consented").getValue();
var isConsented = responseValue.castToBoolean(responseValue);
if (!isConsented.hasValue()) {
return TtpConsentStatus.FAILED_TO_ASK;
}
if (isConsented.booleanValue()) {
return TtpConsentStatus.CONSENTED;
} else {
return TtpConsentStatus.CONSENT_MISSING_OR_REJECTED;
if (response instanceof Parameters responseParameters) {
var responseValue = responseParameters.getParameter("consented").getValue();
var isConsented = responseValue.castToBoolean(responseValue);
if (!isConsented.hasValue()) {
return TtpConsentStatus.FAILED_TO_ASK;
}
if (isConsented.booleanValue()) {
return TtpConsentStatus.CONSENTED;
} else {
return TtpConsentStatus.CONSENT_MISSING_OR_REJECTED;
}
} else if (response instanceof OperationOutcome outcome) {
log.error(
"failed to get consent status from ttp. probably configuration error. outcome: ",
fhirContext.newJsonParser().encodeToString(outcome));
}
} catch (DataFormatException dfe) {
log.error("failed to parse response to FHIR R4 resource.", dfe);
}
return TtpConsentStatus.FAILED_TO_ASK;
}
}

View File

@ -92,8 +92,7 @@ data class GIcsConfigProperties(
/**
* Consent Policy which should be used for consent check
*/
val policySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3",
val parameterVersion: String = "1.1"
val policySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3"
) {
companion object {
const val NAME = "app.consent.gics"

View File

@ -45,20 +45,11 @@ class MtbFileRestController(
return ResponseEntity.ok("Test")
}
@PostMapping( consumes = [ MediaType.APPLICATION_JSON_VALUE ] )
@PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE])
fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity<Unit> {
var ttpConsentStatus = constService.isConsented(mtbFile.patient.id)
// received status REJECTED overrides TTP value. Also in case of disabled consent service,
// we need to override IGNORED status
if (mtbFile.consent.status == Consent.Status.REJECTED) ttpConsentStatus =
TtpConsentStatus.CONSENT_MISSING_OR_REJECTED
val isConsentOK = mtbFile.consent.status == Consent.Status.ACTIVE && (ttpConsentStatus.equals(
TtpConsentStatus.CONSENTED
) || ttpConsentStatus.equals(
TtpConsentStatus.IGNORED
))
val consentStatusBooleanPair = checkConsentStatus(mtbFile)
var ttpConsentStatus = consentStatusBooleanPair.first
val isConsentOK = consentStatusBooleanPair.second
if (isConsentOK) {
logger.debug("Accepted MTB File (bwHC V1) for processing")
requestProcessor.processMtbFile(mtbFile)
@ -71,7 +62,22 @@ class MtbFileRestController(
return ResponseEntity.accepted().build()
}
@PostMapping( consumes = [ CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] )
private fun checkConsentStatus(mtbFile: MtbFile): Pair<TtpConsentStatus, Boolean> {
var ttpConsentStatus = constService.isConsented(mtbFile.patient.id)
val isConsentOK =
(ttpConsentStatus.equals(TtpConsentStatus.IGNORED) && mtbFile.consent.status == Consent.Status.ACTIVE) ||
ttpConsentStatus.equals(
TtpConsentStatus.CONSENTED
)
if (ttpConsentStatus.equals(TtpConsentStatus.IGNORED) && mtbFile.consent.status == Consent.Status.REJECTED) {
// in case ttp check is disabled - we propagate rejected status anyway
ttpConsentStatus = TtpConsentStatus.CONSENT_MISSING_OR_REJECTED
}
return Pair(ttpConsentStatus, isConsentOK)
}
@PostMapping(consumes = [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)

View File

@ -1,14 +1,102 @@
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 org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.OperationOutcome;
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.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.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.client.MockRestServiceServer;
//@ExtendWith(MockitoExtension.class)
@ContextConfiguration(classes = {GicsConsentService.class,
AppConfiguration.class, ObjectMapper.class})
@TestPropertySource(properties = {"app.consent.gics.enabled=true",
"app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"})
@RestClientTest
public class GicsConsentServiceTest {
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;
@BeforeEach
public void setUp() {
mockRestServiceServer = MockRestServiceServer.createServer(appConfiguration.restTemplate());
}
@Test
void isConsented() {
final Parameters responseConsented = 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));
var consentStatus = gicsConsentService.isConsented("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENTED);
}
@Test
void consentRevoced() {
final Parameters responseRevoced = 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));
var consentStatus = gicsConsentService.isConsented("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED);
}
@Test
void gicsParameterInvalid() {
final OperationOutcome responseErrorOutcome = 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));
var consentStatus = gicsConsentService.isConsented("123456");
assertThat(consentStatus).isEqualTo(TtpConsentStatus.FAILED_TO_ASK);
}
}

View File

@ -21,22 +21,29 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
import dev.dnpm.etl.processor.consent.ConsentCheckedIgnored
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import de.ukw.ccc.bwhc.dto.Consent.Status
import dev.dnpm.etl.processor.CustomMediaType
import dev.dnpm.etl.processor.consent.ConsentCheckedIgnored
import dev.dnpm.etl.processor.consent.GicsConsentService
import dev.dnpm.etl.processor.consent.TtpConsentStatus
import dev.dnpm.etl.processor.services.RequestProcessor
import dev.pcvolkmer.mv64e.mtb.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.ValueSource
import org.mockito.Mock
import org.mockito.Mockito.*
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.http.MediaType
import org.springframework.test.context.TestPropertySource
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.delete
import org.springframework.test.web.servlet.post
@ -54,6 +61,7 @@ class MtbFileRestControllerTest {
private lateinit var requestProcessor: RequestProcessor
@BeforeEach
fun setup(
@Mock requestProcessor: RequestProcessor
@ -66,7 +74,7 @@ class MtbFileRestControllerTest {
@Test
fun shouldProcessPostRequest() {
mockMvc.post("/mtbfile") {
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
@ -81,7 +89,7 @@ class MtbFileRestControllerTest {
fun shouldProcessPostRequestWithRejectedConsent() {
mockMvc.post("/mtbfile") {
content =
objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
@ -110,6 +118,93 @@ class MtbFileRestControllerTest {
}
}
@TestPropertySource(
properties = ["app.consent.gics.enabled=true",
"app.consent.gics.gIcsBaseUri=http://localhost:8090/ttp-fhir/fhir/gics"]
)
@Nested
inner class BwhcRequestsCheckConsentViaTtp {
private lateinit var mockMvc: MockMvc
private lateinit var requestProcessor: RequestProcessor
private lateinit var gicsConsentService: GicsConsentService
@BeforeEach
fun setup(
@Mock requestProcessor: RequestProcessor,
@Mock gicsConsentService: GicsConsentService
) {
this.requestProcessor = requestProcessor
val controller = MtbFileRestController(requestProcessor, gicsConsentService)
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
this.gicsConsentService = gicsConsentService
}
@ParameterizedTest
@ValueSource(strings = ["ACTIVE", "REJECTED"])
fun shouldProcessPostRequest(status: String) {
whenever(gicsConsentService.isConsented(any())).thenReturn(TtpConsentStatus.CONSENTED)
mockMvc.post("/mtbfile") {
content =
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status)))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
isAccepted()
}
}
verify(requestProcessor, times(1)).processMtbFile(any<MtbFile>())
}
@ParameterizedTest
@ValueSource(strings = ["ACTIVE", "REJECTED"])
fun shouldProcessPostRequestWithRejectedConsent(status: String) {
whenever(gicsConsentService.isConsented(any())).thenReturn(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED)
mockMvc.post("/mtbfile") {
content =
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.valueOf(status)))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
isAccepted()
}
}
// consent status from ttp should override file consent value
verify(requestProcessor, times(1)).processDeletion(
anyValueClass(),
org.mockito.kotlin.eq(TtpConsentStatus.CONSENT_MISSING_OR_REJECTED)
)
}
@Test
fun shouldProcessDeleteRequest() {
mockMvc.delete("/mtbfile/TEST_12345678").andExpect {
status {
isAccepted()
}
}
verify(requestProcessor, times(1)).processDeletion(
anyValueClass(),
org.mockito.kotlin.eq(TtpConsentStatus.IGNORED)
)
verify(gicsConsentService, times(0)).isConsented(any())
}
}
@Nested
inner class BwhcRequestsWithAlias {
@ -129,7 +224,7 @@ class MtbFileRestControllerTest {
@Test
fun shouldProcessPostRequest() {
mockMvc.post("/mtb") {
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.ACTIVE))
content = objectMapper.writeValueAsString(bwhcMtbFileContent(Status.ACTIVE))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
@ -144,7 +239,7 @@ class MtbFileRestControllerTest {
fun shouldProcessPostRequestWithRejectedConsent() {
mockMvc.post("/mtb") {
content =
objectMapper.writeValueAsString(bwhcMtbFileContent(Consent.Status.REJECTED))
objectMapper.writeValueAsString(bwhcMtbFileContent(Status.REJECTED))
contentType = MediaType.APPLICATION_JSON
}.andExpect {
status {
@ -212,7 +307,7 @@ class MtbFileRestControllerTest {
}
companion object {
fun bwhcMtbFileContent(consentStatus: Consent.Status) = MtbFile.builder()
fun bwhcMtbFileContent(consentStatus: Status) = MtbFile.builder()
.withPatient(
Patient.builder()
.withId("TEST_12345678")