From b27670535f81de3a7659ae7f3806c5effdcef0c7 Mon Sep 17 00:00:00 2001 From: Jakub Lidke Date: Mon, 28 Apr 2025 11:44:26 +0200 Subject: [PATCH] feat: check consent via gICS before process mtb file --- build.gradle.kts | 6 +- dev-compose.yml | 44 ++++- dev/docker-compose.dev.yml | 74 +++++---- .../input/MtbFileRestControllerTest.kt | 23 ++- .../pseudonym/GpasPseudonymGeneratorTest.kt | 18 +- .../consent/ConsentCheckedIgnored.java | 9 + .../etl/processor/consent/ConsentStatus.java | 8 + .../processor/consent/GicsConsentService.java | 157 ++++++++++++++++++ .../etl/processor/consent/ICheckConsent.java | 8 + .../pseudonym/GpasPseudonymGenerator.java | 26 +-- .../processor/config/AppConfigProperties.kt | 44 +++++ .../etl/processor/config/AppConfiguration.kt | 41 ++++- .../etl/processor/config/AppFhirConfig.kt | 16 ++ .../etl/processor/input/KafkaInputListener.kt | 11 +- .../processor/input/MtbFileRestController.kt | 23 ++- .../etl/processor/monitoring/RequestStatus.kt | 3 +- .../processor/services/RequestProcessor.kt | 28 +++- .../consent/GicsConsentServiceTest.java | 14 ++ .../processor/input/KafkaInputListenerTest.kt | 83 +++++++-- .../input/MtbFileRestControllerTest.kt | 20 ++- .../services/RequestProcessorTest.kt | 9 +- 21 files changed, 562 insertions(+), 103 deletions(-) create mode 100644 src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckedIgnored.java create mode 100644 src/main/java/dev/dnpm/etl/processor/consent/ConsentStatus.java create mode 100644 src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java create mode 100644 src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java create mode 100644 src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt create mode 100644 src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 2207e81..0711328 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,8 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.tasks.bundling.BootBuildImage +import java.text.SimpleDateFormat +import java.util.* plugins { war @@ -133,6 +135,7 @@ tasks.jacocoTestReport { } } + tasks.named("bootBuildImage") { imageName.set("ghcr.io/ccc-mf/etl-processor") @@ -143,7 +146,8 @@ tasks.named("bootBuildImage") { environment.set(environment.get() + mapOf( // Enable this line to embed CA Certs into image on build time - //"BP_EMBED_CERTS" to "true", + "BP_EMBED_CERTS" to "true", + "BP_OCI_CREATED" to SimpleDateFormat("MM-dd-yyyy_hh-mm").format(Date()), "BP_OCI_SOURCE" to "https://github.com/CCC-MF/etl-processor", "BP_OCI_LICENSES" to "AGPLv3", "BP_OCI_DESCRIPTION" to "ETL Processor for bwHC MTB files" diff --git a/dev-compose.yml b/dev-compose.yml index e2dfdb6..ef76c49 100644 --- a/dev-compose.yml +++ b/dev-compose.yml @@ -16,6 +16,11 @@ services: KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093 KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + healthcheck: + test: kafka-topics --bootstrap-server kafka:9092 --list + interval: 30s + timeout: 10s + retries: 3 ## Use AKHQ as Kafka web frontend akhq: @@ -53,4 +58,41 @@ services: # environment: # POSTGRES_DB: dev # POSTGRES_USER: dev -# POSTGRES_PASSWORD: dev \ No newline at end of file +# 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 \ No newline at end of file diff --git a/dev/docker-compose.dev.yml b/dev/docker-compose.dev.yml index d7a436b..f8f9183 100644 --- a/dev/docker-compose.dev.yml +++ b/dev/docker-compose.dev.yml @@ -2,31 +2,55 @@ version: '3.7' services: - zoo1: - image: zookeeper:3.8.0 - hostname: zoo1 + zoo: + image: zookeeper:3.9.2 + restart: unless-stopped ports: - "2181:2181" environment: ZOO_MY_ID: 1 ZOO_PORT: 2181 - ZOO_SERVERS: server.1=zoo1:2888:3888;2181 + ZOO_SERVERS: server.1=zoo:2888:3888;2181 - kafka1: - image: confluentinc/cp-kafka:7.2.1 - hostname: kafka1 + kafka: + image: confluentinc/cp-kafka:7.6.1 ports: - "9092:9092" environment: - KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka1:19092,LISTENER_DOCKER_EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://kafka:19092,LISTENER_DOCKER_EXTERNAL://172.17.0.1:9093,LISTENER_EXTERNAL://127.0.0.1:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT,LISTENER_EXTERNAL:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL - KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181" + KAFKA_ZOOKEEPER_CONNECT: zoo:2181 KAFKA_BROKER_ID: 1 - KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" + KAFKA_LOG4J_LOGGERS: kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_MESSAGE_MAX_BYTES: 5242880 + KAFKA_REPLICA_FETCH_MAX_BYTES: 5242880 + KAFKA_COMPRESSION_TYPE: gzip depends_on: - - zoo1 + - zoo + healthcheck: + test: kafka-topics --bootstrap-server kafka:9092 --list + interval: 30s + timeout: 10s + retries: 3 + + akhq: + image: tchiotludo/akhq:0.25.0 + environment: + AKHQ_CONFIGURATION: | + akhq: + ui-options: + topic.show-all-consumer-groups: true + topic-data.sort: NEWEST + connections: + docker-kafka-server: + properties: + bootstrap.servers: "kafka:19092" + ports: + - "9000:8080" + depends_on: + - kafka kafka-rest-proxy: image: confluentinc/cp-kafka-rest:7.2.1 @@ -40,8 +64,8 @@ services: KAFKA_REST_HOST_NAME: kafka-rest-proxy KAFKA_REST_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:19092 depends_on: - - zoo1 - - kafka1 + - zoo + - kafka kafka-connect: image: confluentinc/cp-kafka-connect:7.2.1 @@ -67,24 +91,6 @@ services: #volumes: # - ./connectors:/etc/kafka-connect/jars/ depends_on: - - zoo1 - - kafka1 + - zoo + - kafka - kafka-rest-proxy - - akhq: - image: tchiotludo/akhq:0.21.0 - environment: - AKHQ_CONFIGURATION: | - akhq: - connections: - docker-kafka-server: - properties: - bootstrap.servers: "kafka1:19092" - connect: - - name: "kafka-connect" - url: "http://kafka-connect:8083" - ports: - - "8084:8080" - depends_on: - - kafka1 - - kafka-connect diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt index 670020f..276c35f 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -22,7 +22,11 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import dev.dnpm.etl.processor.anyValueClass +import dev.dnpm.etl.processor.config.AppFhirConfig import dev.dnpm.etl.processor.config.AppSecurityConfiguration +import dev.dnpm.etl.processor.consent.ConsentCheckedIgnored +import dev.dnpm.etl.processor.consent.ConsentStatus +import dev.dnpm.etl.processor.consent.ICheckConsent import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.services.RequestProcessor @@ -30,11 +34,9 @@ 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.mockito.Mockito import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.any -import org.mockito.kotlin.never -import org.mockito.kotlin.times -import org.mockito.kotlin.verify +import org.mockito.kotlin.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean @@ -54,7 +56,8 @@ import org.springframework.test.web.servlet.post @ContextConfiguration( classes = [ MtbFileRestController::class, - AppSecurityConfiguration::class + AppSecurityConfiguration::class, + ConsentCheckedIgnored::class, ICheckConsent::class ] ) @MockBean(TokenRepository::class, RequestProcessor::class) @@ -63,7 +66,8 @@ import org.springframework.test.web.servlet.post "app.pseudonymize.generator=BUILDIN", "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", - "app.security.enable-tokens=true" + "app.security.enable-tokens=true", + "app.consent.gics.enabled=false" ] ) class MtbFileRestControllerTest { @@ -141,7 +145,7 @@ class MtbFileRestControllerTest { status { isAccepted() } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(ConsentStatus.IGNORED)) } @Test @@ -152,7 +156,7 @@ class MtbFileRestControllerTest { status { isUnauthorized() } } - verify(requestProcessor, never()).processDeletion(anyValueClass()) + verify(requestProcessor, never()).processDeletion(anyValueClass(), any()) } @Nested @@ -163,7 +167,8 @@ 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.security.enable-oidc=true", + "app.consent.gics.enabled=false" ] ) inner class WithOidcEnabled { diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt index da0c55c..dc5a725 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt @@ -19,6 +19,7 @@ package dev.dnpm.etl.processor.pseudonym +import dev.dnpm.etl.processor.config.AppFhirConfig import dev.dnpm.etl.processor.config.GPasConfigProperties import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -42,6 +43,7 @@ class GpasPseudonymGeneratorTest { private lateinit var mockRestServiceServer: MockRestServiceServer private lateinit var generator: GpasPseudonymGenerator private lateinit var restTemplate: RestTemplate + private var appFhirConfig: AppFhirConfig = AppFhirConfig() @BeforeEach fun setup() { @@ -56,7 +58,8 @@ class GpasPseudonymGeneratorTest { this.restTemplate = RestTemplate() this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) - this.generator = GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate) + this.generator = + GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate, appFhirConfig) } @Test @@ -65,7 +68,13 @@ class GpasPseudonymGeneratorTest { method(HttpMethod.POST) requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate") }.andRespond { - withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890")) + withStatus(HttpStatus.OK).body( + getDummyResponseBody( + "1234", + "test", + "test1234ABCDEF567890" + ) + ) .createResponse(it) } @@ -91,7 +100,10 @@ class GpasPseudonymGeneratorTest { requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate") }.andRespond { withStatus(HttpStatus.FOUND) - .header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate") + .header( + HttpHeaders.LOCATION, + "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate" + ) .createResponse(it) } diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckedIgnored.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckedIgnored.java new file mode 100644 index 0000000..03309cc --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentCheckedIgnored.java @@ -0,0 +1,9 @@ +package dev.dnpm.etl.processor.consent; + +public class ConsentCheckedIgnored implements ICheckConsent{ + + @Override + public ConsentStatus isConsented(String personIdentifierValue) { + return ConsentStatus.IGNORED; + } +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ConsentStatus.java b/src/main/java/dev/dnpm/etl/processor/consent/ConsentStatus.java new file mode 100644 index 0000000..50048a7 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/ConsentStatus.java @@ -0,0 +1,8 @@ +package dev.dnpm.etl.processor.consent; + +public enum ConsentStatus { + CONSENTED, + CONSENT_MISSING, + FAILED_TO_ASK, + IGNORED, CONSENT_REJECTED +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java new file mode 100644 index 0000000..36fc6f5 --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/GicsConsentService.java @@ -0,0 +1,157 @@ +package dev.dnpm.etl.processor.consent; + +import ca.uhn.fhir.context.FhirContext; +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.Coding; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Parameters; +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; +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; + + +public class GicsConsentService implements ICheckConsent { + + private final Logger log = LoggerFactory.getLogger(GicsConsentService.class); + + private final GIcsConfigProperties gIcsConfigProperties; + + public final String IS_CONSENTED_PATH = "/ttp-fhir/fhir/gics/$isConsented"; + private final RetryTemplate retryTemplate; + private final RestTemplate restTemplate; + private final FhirContext fhirContext; + private final HttpHeaders httpHeader; + private String url; + + + public GicsConsentService(GIcsConfigProperties gIcsConfigProperties, + RetryTemplate retryTemplate, RestTemplate restTemplate, AppFhirConfig appFhirConfig) { + this.gIcsConfigProperties = gIcsConfigProperties; + this.retryTemplate = retryTemplate; + this.restTemplate = restTemplate; + this.fhirContext = appFhirConfig.fhirContext(); + httpHeader = buildHeader(gIcsConfigProperties.getUsername(), + gIcsConfigProperties.getPassword()); + } + + public String getGicsUri() { + if (url == null) { + final String gIcsBaseUri = gIcsConfigProperties.getGIcsBaseUri(); + if (StringUtils.isBlank(gIcsBaseUri)) { + throw new IllegalArgumentException( + "gICS base URL is empty - should call gICS with false configuration."); + } + url = UriComponentsBuilder.fromHttpUrl(gIcsBaseUri) + .path(IS_CONSENTED_PATH) + .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; + } + + public static Parameters getIsConsentedParam(GIcsConfigProperties configProperties, + 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.getConsentDomainName()))); + result.addParameter(new ParametersParameterComponent().setName("policy") + .setValue(new Coding().setCode(configProperties.getPolicyCode()) + .setSystem(configProperties.getPolicySystem()))); + result.addParameter(new ParametersParameterComponent().setName("version") + .setValue(new StringType().setValue(configProperties.getParameterVersion()))); + return result; + } + + protected String getConsentStatusResponse(Parameters parameter) { + var parameterAsXml = fhirContext.newXmlParser().encodeResourceToString(parameter); + + HttpEntity requestEntity = new HttpEntity<>(parameterAsXml, this.httpHeader); + ResponseEntity responseEntity; + try { + responseEntity = retryTemplate.execute( + ctx -> restTemplate.exchange(getGicsUri(), HttpMethod.POST, requestEntity, + String.class)); + } catch (RestClientException e) { + var msg = String.format("Get consents status request failed reason: '%s", + e.getMessage()); + log.error(msg); + return null; + + } catch (TerminatedRetryException terminatedRetryException) { + var msg = String.format( + "Get consents status process has been terminated. termination reason: '%s", + 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 ConsentStatus isConsented(String personIdentifierValue) { + var parameter = GicsConsentService.getIsConsentedParam(gIcsConfigProperties, + personIdentifierValue); + + var consentStatusResponse = getConsentStatusResponse(parameter); + return evaluateConsentResponse(consentStatusResponse); + + } + + private ConsentStatus evaluateConsentResponse(String consentStatusResponse) { + if (consentStatusResponse == null) { + return ConsentStatus.FAILED_TO_ASK; + } + var responseParameters = fhirContext.newJsonParser() + .parseResource(Parameters.class, consentStatusResponse); + + var responseValue = responseParameters.getParameter("consented").getValue(); + var isConsented = responseValue.castToBoolean(responseValue); + if (!isConsented.hasValue()) { + return ConsentStatus.FAILED_TO_ASK; + } + if (isConsented.booleanValue()) { + return ConsentStatus.CONSENTED; + } else { + return ConsentStatus.CONSENT_MISSING; + } + } +} diff --git a/src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java b/src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java new file mode 100644 index 0000000..af7955a --- /dev/null +++ b/src/main/java/dev/dnpm/etl/processor/consent/ICheckConsent.java @@ -0,0 +1,8 @@ +package dev.dnpm.etl.processor.consent; + + +public interface ICheckConsent { + + ConsentStatus isConsented(String personIdentifierValue); + +} diff --git a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java index 77caa77..af4c55f 100644 --- a/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java +++ b/src/main/java/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGenerator.java @@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.pseudonym; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; +import dev.dnpm.etl.processor.config.AppFhirConfig; import dev.dnpm.etl.processor.config.GPasConfigProperties; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.Identifier; @@ -36,7 +37,7 @@ import org.springframework.web.client.RestTemplate; public class GpasPseudonymGenerator implements Generator { - private final static FhirContext r4Context = FhirContext.forR4(); + private final FhirContext r4Context; private final String gPasUrl; private final String psnTargetDomain; private final HttpHeaders httpHeader; @@ -45,11 +46,13 @@ public class GpasPseudonymGenerator implements Generator { private final RestTemplate restTemplate; - public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, RestTemplate restTemplate) { + public GpasPseudonymGenerator(GPasConfigProperties gpasCfg, RetryTemplate retryTemplate, + RestTemplate restTemplate, AppFhirConfig appFhirConfig) { this.retryTemplate = retryTemplate; this.restTemplate = restTemplate; this.gPasUrl = gpasCfg.getUri(); this.psnTargetDomain = gpasCfg.getTarget(); + this.r4Context = appFhirConfig.fhirContext(); httpHeader = getHttpHeaders(gpasCfg.getUsername(), gpasCfg.getPassword()); log.debug(String.format("%s has been initialized", this.getClass().getName())); @@ -61,7 +64,7 @@ public class GpasPseudonymGenerator implements Generator { var gPasRequestBody = getGpasRequestBody(id); var responseEntity = getGpasPseudonym(gPasRequestBody); var gPasPseudonymResult = (Parameters) r4Context.newJsonParser() - .parseResource(responseEntity.getBody()); + .parseResource(responseEntity.getBody()); return unwrapPseudonym(gPasPseudonymResult); } @@ -75,9 +78,9 @@ public class GpasPseudonymGenerator implements Generator { } final var identifier = (Identifier) parameters.get().getPart().stream() - .filter(a -> a.getName().equals("pseudonym")) - .findFirst() - .orElseGet(ParametersParameterComponent::new).getValue(); + .filter(a -> a.getName().equals("pseudonym")) + .findFirst() + .orElseGet(ParametersParameterComponent::new).getValue(); // pseudonym return sanitizeValue(identifier.getValue()); @@ -97,7 +100,6 @@ public class GpasPseudonymGenerator implements Generator { return psnValue.replaceAll(forbiddenCharsRegex, "_"); } - @NotNull protected ResponseEntity getGpasPseudonym(String gPasRequestBody) { @@ -106,8 +108,8 @@ public class GpasPseudonymGenerator implements Generator { try { responseEntity = retryTemplate.execute( - ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity, - String.class)); + ctx -> restTemplate.exchange(gPasUrl, HttpMethod.POST, requestEntity, + String.class)); if (responseEntity.getStatusCode().is2xxSuccessful()) { log.debug("API request succeeded. Response: {}", responseEntity.getStatusCode()); @@ -119,16 +121,16 @@ public class GpasPseudonymGenerator implements Generator { return responseEntity; } catch (Exception unexpected) { throw new PseudonymRequestFailed( - "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); + "API request due unexpected error unsuccessful gPas unsuccessful.", unexpected); } } protected String getGpasRequestBody(String id) { var requestParameters = new Parameters(); requestParameters.addParameter().setName("target") - .setValue(new StringType().setValue(psnTargetDomain)); + .setValue(new StringType().setValue(psnTargetDomain)); requestParameters.addParameter().setName("original") - .setValue(new StringType().setValue(id)); + .setValue(new StringType().setValue(id)); final IParser iParser = r4Context.newJsonParser(); return iParser.encodeResourceToString(requestParameters); } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt index d951c60..f04d30f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -66,6 +66,50 @@ data class GPasConfigProperties( } } +@ConfigurationProperties(GIcsConfigProperties.NAME) +data class GIcsConfigProperties( + /** + * Base URL to gICS System + * + */ + val gIcsBaseUri: String?, + val username: String?, + val password: String?, + + /** + * If value is 'true' valid consent at processing time is mandatory for transmission of DNPM + * files otherwise they will be flagged and skipped. + * If value 'false' or missing consent status is assumed to be valid. + */ + val enabled: Boolean?, + + /** + * gICS specific system + * **/ + val personIdentifierSystem: String = + "https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID", + + /** + * Domain of consent resources + * **/ + val consentDomainName: String = "MII", + + /** + * Value to expect in case of positiv consent + */ + val policyCode: String = "2.16.840.1.113883.3.1937.777.24.5.3.6", + + /** + * 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" +) { + companion object { + const val NAME = "app.consent.gics" + } +} + @ConfigurationProperties(RestTargetProperties.NAME) data class RestTargetProperties( val uri: String?, diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt index 5fc1120..56cca9a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -20,6 +20,9 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.databind.ObjectMapper +import dev.dnpm.etl.processor.consent.ConsentCheckedIgnored +import dev.dnpm.etl.processor.consent.ICheckConsent +import dev.dnpm.etl.processor.consent.GicsConsentService import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult import dev.dnpm.etl.processor.monitoring.ConnectionCheckService import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService @@ -73,7 +76,8 @@ import kotlin.time.toJavaDuration value = [ AppConfigProperties::class, PseudonymizeConfigProperties::class, - GPasConfigProperties::class + GPasConfigProperties::class, + GIcsConfigProperties::class ] ) @EnableScheduling @@ -86,22 +90,27 @@ class AppConfiguration { return RestTemplate() } + @Bean + fun appFhirConfig(): AppFhirConfig{ + return AppFhirConfig() + } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") @Bean - fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator { + fun gpasPseudonymGenerator(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): Generator { try { if (!configProperties.sslCaLocation.isNullOrBlank()) { return GpasPseudonymGenerator( configProperties, retryTemplate, - createCustomGpasRestTemplate(configProperties) + createCustomGpasRestTemplate(configProperties),appFhirConfig ) } } catch (e: Exception) { throw RuntimeException(e) } - return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate) + return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate,appFhirConfig) } @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "BUILDIN", matchIfMissing = true) @@ -113,20 +122,20 @@ class AppConfiguration { @ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS") @ConditionalOnMissingBean @Bean - fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate): Generator { + fun gpasPseudonymGeneratorOnDeprecatedProperty(configProperties: GPasConfigProperties, retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): Generator { try { if (!configProperties.sslCaLocation.isNullOrBlank()) { return GpasPseudonymGenerator( configProperties, retryTemplate, - createCustomGpasRestTemplate(configProperties) + createCustomGpasRestTemplate(configProperties),appFhirConfig ) } } catch (e: Exception) { throw RuntimeException(e) } - return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate) + return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate,appFhirConfig) } private fun createCustomGpasRestTemplate(configProperties: GPasConfigProperties): RestTemplate { @@ -279,5 +288,23 @@ class AppConfiguration { fun jdbcConfiguration(): AbstractJdbcConfiguration { return AppJdbcConfiguration() } + + @Bean + @ConditionalOnMissingBean + fun constService(): ICheckConsent { + return ConsentCheckedIgnored() + } + + @Bean + @ConditionalOnProperty(name = ["app.consent.gics.enabled"], havingValue = "true") + fun gicsAccessConsent( gIcsConfigProperties: GIcsConfigProperties, + retryTemplate: RetryTemplate, restTemplate: RestTemplate, appFhirConfig: AppFhirConfig): ICheckConsent { + return GicsConsentService( + gIcsConfigProperties, + retryTemplate, + restTemplate, + appFhirConfig + ) + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt new file mode 100644 index 0000000..2b5ff8f --- /dev/null +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt @@ -0,0 +1,16 @@ +package dev.dnpm.etl.processor.config + +import ca.uhn.fhir.context.FhirContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class AppFhirConfig { + private val fhirCtx: FhirContext = FhirContext.forR4() + + @Bean + fun fhirContext(): FhirContext { + return fhirCtx + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt index 2aff8cb..af97b72 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt @@ -24,6 +24,7 @@ import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.PatientId import dev.dnpm.etl.processor.RequestId +import dev.dnpm.etl.processor.consent.ConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import org.apache.kafka.clients.consumer.ConsumerRecord import org.slf4j.LoggerFactory @@ -44,7 +45,7 @@ class KafkaInputListener( } else { RequestId("") } - +// fixme: add consent check if (mtbFile.consent.status == Consent.Status.ACTIVE) { logger.debug("Accepted MTB File for processing") if (requestId.isBlank()) { @@ -55,9 +56,13 @@ class KafkaInputListener( } else { logger.debug("Accepted MTB File and process deletion") if (requestId.isBlank()) { - requestProcessor.processDeletion(patientId) + requestProcessor.processDeletion(patientId, ConsentStatus.IGNORED) } else { - requestProcessor.processDeletion(patientId, requestId) + requestProcessor.processDeletion( + patientId, + requestId, + ConsentStatus.IGNORED + ) } } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt index 9e282c2..de4950b 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -22,6 +22,8 @@ package dev.dnpm.etl.processor.input import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.PatientId +import dev.dnpm.etl.processor.consent.ICheckConsent +import dev.dnpm.etl.processor.consent.ConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import org.slf4j.LoggerFactory import org.springframework.http.ResponseEntity @@ -30,7 +32,7 @@ import org.springframework.web.bind.annotation.* @RestController @RequestMapping(path = ["mtbfile"]) class MtbFileRestController( - private val requestProcessor: RequestProcessor, + private val requestProcessor: RequestProcessor, private val constService: ICheckConsent ) { private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) @@ -42,13 +44,24 @@ class MtbFileRestController( @PostMapping fun mtbFile(@RequestBody mtbFile: MtbFile): ResponseEntity { - if (mtbFile.consent.status == Consent.Status.ACTIVE) { + + var consentStatus = constService.isConsented(mtbFile.patient.id) + if (mtbFile.consent.status == Consent.Status.ACTIVE && (consentStatus.equals(ConsentStatus.CONSENTED) || consentStatus.equals( + ConsentStatus.IGNORED + )) + ) { logger.debug("Accepted MTB File for processing") requestProcessor.processMtbFile(mtbFile) } else { - logger.debug("Accepted MTB File and process deletion") + var msg = "Accepted MTB File and process deletion" + if (!consentStatus.equals(ConsentStatus.CONSENTED) || consentStatus.equals(ConsentStatus.IGNORED)) { + msg = "Accepted MTB File. But consent is missing, therefore process deletion." + } + if (mtbFile.consent.status == Consent.Status.REJECTED) consentStatus = + ConsentStatus.CONSENT_REJECTED + logger.debug(msg) val patientId = PatientId(mtbFile.patient.id) - requestProcessor.processDeletion(patientId) + requestProcessor.processDeletion(patientId, consentStatus) } return ResponseEntity.accepted().build() } @@ -56,7 +69,7 @@ class MtbFileRestController( @DeleteMapping(path = ["{patientId}"]) fun deleteData(@PathVariable patientId: String): ResponseEntity { logger.debug("Accepted patient ID to process deletion") - requestProcessor.processDeletion(PatientId(patientId)) + requestProcessor.processDeletion(PatientId(patientId), ConsentStatus.IGNORED) return ResponseEntity.accepted().build() } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt index 8c19e86..85f2278 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt @@ -24,5 +24,6 @@ enum class RequestStatus(val value: String) { WARNING("warning"), ERROR("error"), UNKNOWN("unknown"), - DUPLICATION("duplication") + DUPLICATION("duplication"), + CONSENTMISSING("no-consent") } \ No newline at end of file diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt index f4e6222..29fad50 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.MtbFile import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.config.AppConfigProperties +import dev.dnpm.etl.processor.consent.ConsentStatus import dev.dnpm.etl.processor.monitoring.Report import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus @@ -59,7 +60,8 @@ class RequestProcessor( mtbFile pseudonymizeWith pseudonymizeService mtbFile anonymizeContentWith pseudonymizeService - val request = MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile)) + val request = + MtbFileSender.MtbFileRequest(requestId, transformationService.transform(mtbFile)) val patientPseudonym = PatientPseudonym(request.mtbFile.patient.id) @@ -105,21 +107,29 @@ class RequestProcessor( val lastMtbFileRequestForPatient = requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) - val isLastRequestDeletion = requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) + val isLastRequestDeletion = + requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFile) } - fun processDeletion(patientId: PatientId) { - processDeletion(patientId, randomRequestId()) + fun processDeletion(patientId: PatientId, isConsented: ConsentStatus) { + processDeletion(patientId, randomRequestId(), isConsented) } - fun processDeletion(patientId: PatientId, requestId: RequestId) { + fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: ConsentStatus) { try { val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) + val requestStatus: RequestStatus = when (isConsented) { + ConsentStatus.CONSENT_MISSING -> RequestStatus.CONSENTMISSING + ConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR + ConsentStatus.CONSENTED, ConsentStatus.IGNORED, + ConsentStatus.CONSENT_REJECTED -> RequestStatus.UNKNOWN + } + requestService.save( Request( requestId, @@ -127,11 +137,14 @@ class RequestProcessor( patientId, fingerprint(patientPseudonym.value), RequestType.DELETE, - RequestStatus.UNKNOWN + requestStatus ) ) - val responseStatus = sender.send(MtbFileSender.DeleteRequest(requestId, patientPseudonym)) + val responseStatus = + sender.send(MtbFileSender.DeleteRequest(requestId, patientPseudonym)) + + //fixme: publish proper report if consent check failed applicationEventPublisher.publishEvent( ResponseEvent( @@ -171,5 +184,4 @@ class RequestProcessor( .lowercase() ) } - } \ No newline at end of file diff --git a/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java new file mode 100644 index 0000000..600c6eb --- /dev/null +++ b/src/test/java/dev/dnpm/etl/processor/consent/GicsConsentServiceTest.java @@ -0,0 +1,14 @@ +package dev.dnpm.etl.processor.consent; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +//@ExtendWith(MockitoExtension.class) +public class GicsConsentServiceTest { + + @Test + void isConsented() { + } + +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt index 7753dbc..8df9067 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt @@ -24,6 +24,7 @@ import de.ukw.ccc.bwhc.dto.Consent import de.ukw.ccc.bwhc.dto.MtbFile import de.ukw.ccc.bwhc.dto.Patient import dev.dnpm.etl.processor.anyValueClass +import dev.dnpm.etl.processor.consent.ConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import org.apache.kafka.clients.consumer.ConsumerRecord import org.apache.kafka.common.header.internals.RecordHeader @@ -35,6 +36,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.times import org.mockito.kotlin.verify import java.util.* @@ -48,7 +50,7 @@ class KafkaInputListenerTest { @BeforeEach fun setup( - @Mock requestProcessor: RequestProcessor + @Mock requestProcessor: RequestProcessor, ) { this.requestProcessor = requestProcessor this.objectMapper = ObjectMapper() @@ -63,7 +65,15 @@ class KafkaInputListenerTest { .withConsent(Consent.builder().withStatus(Consent.Status.ACTIVE).build()) .build() - kafkaInputListener.onMessage(ConsumerRecord("testtopic", 0, 0, "", this.objectMapper.writeValueAsString(mtbFile))) + kafkaInputListener.onMessage( + ConsumerRecord( + "testtopic", + 0, + 0, + "", + this.objectMapper.writeValueAsString(mtbFile) + ) + ) verify(requestProcessor, times(1)).processMtbFile(any()) } @@ -75,9 +85,20 @@ class KafkaInputListenerTest { .withConsent(Consent.builder().withStatus(Consent.Status.REJECTED).build()) .build() - kafkaInputListener.onMessage(ConsumerRecord("testtopic", 0, 0, "", this.objectMapper.writeValueAsString(mtbFile))) + kafkaInputListener.onMessage( + ConsumerRecord( + "testtopic", + 0, + 0, + "", + this.objectMapper.writeValueAsString(mtbFile) + ) + ) - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + eq(ConsentStatus.IGNORED) + ) } @Test @@ -87,9 +108,28 @@ class KafkaInputListenerTest { .withConsent(Consent.builder().withStatus(Consent.Status.ACTIVE).build()) .build() - val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray()))) + val headers = RecordHeaders( + listOf( + RecordHeader( + "requestId", + UUID.randomUUID().toString().toByteArray() + ) + ) + ) kafkaInputListener.onMessage( - ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty()) + ConsumerRecord( + "testtopic", + 0, + 0, + -1L, + TimestampType.NO_TIMESTAMP_TYPE, + -1, + -1, + "", + this.objectMapper.writeValueAsString(mtbFile), + headers, + Optional.empty() + ) ) verify(requestProcessor, times(1)).processMtbFile(any(), anyValueClass()) @@ -102,11 +142,34 @@ class KafkaInputListenerTest { .withConsent(Consent.builder().withStatus(Consent.Status.REJECTED).build()) .build() - val headers = RecordHeaders(listOf(RecordHeader("requestId", UUID.randomUUID().toString().toByteArray()))) - kafkaInputListener.onMessage( - ConsumerRecord("testtopic", 0, 0, -1L, TimestampType.NO_TIMESTAMP_TYPE, -1, -1, "", this.objectMapper.writeValueAsString(mtbFile), headers, Optional.empty()) + val headers = RecordHeaders( + listOf( + RecordHeader( + "requestId", + UUID.randomUUID().toString().toByteArray() + ) + ) + ) + kafkaInputListener.onMessage( + ConsumerRecord( + "testtopic", + 0, + 0, + -1L, + TimestampType.NO_TIMESTAMP_TYPE, + -1, + -1, + "", + this.objectMapper.writeValueAsString(mtbFile), + headers, + Optional.empty() + ) + ) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + anyValueClass(), + eq(ConsentStatus.IGNORED) ) - verify(requestProcessor, times(1)).processDeletion(anyValueClass(), anyValueClass()) } } \ No newline at end of file diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt index f9fe3f3..5c13c96 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -22,13 +22,14 @@ package dev.dnpm.etl.processor.input import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import dev.dnpm.etl.processor.anyValueClass +import dev.dnpm.etl.processor.consent.ConsentCheckedIgnored +import dev.dnpm.etl.processor.consent.ConsentStatus import dev.dnpm.etl.processor.services.RequestProcessor import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock -import org.mockito.Mockito.times -import org.mockito.Mockito.verify +import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.any import org.springframework.http.MediaType @@ -51,7 +52,10 @@ class MtbFileRestControllerTest { @Mock requestProcessor: RequestProcessor ) { this.requestProcessor = requestProcessor - val controller = MtbFileRestController(requestProcessor) + val controller = MtbFileRestController( + requestProcessor, ConsentCheckedIgnored() + ) + this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build() } @@ -128,7 +132,10 @@ class MtbFileRestControllerTest { } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(ConsentStatus.CONSENT_REJECTED) + ) } @Test @@ -139,7 +146,10 @@ class MtbFileRestControllerTest { } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass()) + verify(requestProcessor, times(1)).processDeletion( + anyValueClass(), + org.mockito.kotlin.eq(ConsentStatus.IGNORED) + ) } } \ No newline at end of file diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt index 1c58d5d..af1d80e 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.ukw.ccc.bwhc.dto.* import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.config.AppConfigProperties +import dev.dnpm.etl.processor.consent.ConsentStatus import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType @@ -337,7 +338,7 @@ class RequestProcessorTest { MtbFileSender.Response(status = RequestStatus.UNKNOWN) }.whenever(sender).send(any()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED) val requestCaptor = argumentCaptor() verify(requestService, times(1)).save(requestCaptor.capture()) @@ -355,7 +356,7 @@ class RequestProcessorTest { MtbFileSender.Response(status = RequestStatus.SUCCESS) }.whenever(sender).send(any()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -373,7 +374,7 @@ class RequestProcessorTest { MtbFileSender.Response(status = RequestStatus.ERROR) }.whenever(sender).send(any()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED) val eventCaptor = argumentCaptor() verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) @@ -385,7 +386,7 @@ class RequestProcessorTest { fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() { doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - this.requestProcessor.processDeletion(TEST_PATIENT_ID) + this.requestProcessor.processDeletion(TEST_PATIENT_ID, isConsented = ConsentStatus.IGNORED) val requestCaptor = argumentCaptor() verify(requestService, times(1)).save(requestCaptor.capture())