diff options
83 files changed, 5601 insertions, 5652 deletions
diff --git a/build.gradle.kts b/build.gradle.kts index 07979b5..683de60 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -203,4 +203,17 @@ spotless { removeUnusedImports() googleJavaFormat() } + kotlin { + ktlint() + suppressLintsFor { + step = "ktlint" + shortCode = "standard:filename" + } + suppressLintsFor { + step = "ktlint" + shortCode = "standard:no-wildcard-imports" + } + leadingTabsToSpaces() + endWithNewline() + } }
\ No newline at end of file diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/AbstractTestcontainerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/AbstractTestcontainerTest.kt index 13b57d0..136691e 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/AbstractTestcontainerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/AbstractTestcontainerTest.kt @@ -25,13 +25,13 @@ import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.junit.jupiter.Container abstract class AbstractTestcontainerTest { - companion object { @Container - val dbContainer = CustomPostgreSQLContainer("postgres:10-alpine") - .withDatabaseName("test") - .withUsername("test") - .withPassword("test") ?: throw RuntimeException("Failed to create testcontainer!") + val dbContainer = + CustomPostgreSQLContainer("postgres:10-alpine") + .withDatabaseName("test") + .withUsername("test") + .withPassword("test") ?: throw RuntimeException("Failed to create testcontainer!") @DynamicPropertySource @JvmStatic @@ -41,11 +41,12 @@ abstract class AbstractTestcontainerTest { registry.add("spring.datasource.password", dbContainer::getPassword) } } - } -class CustomPostgreSQLContainer(dockerImageName: String) : PostgreSQLContainer<CustomPostgreSQLContainer>(dockerImageName) { +class CustomPostgreSQLContainer( + dockerImageName: String, +) : PostgreSQLContainer<CustomPostgreSQLContainer>(dockerImageName) { override fun stop() { // Keep Testcontainer alive until JVM destroys it } -}
\ No newline at end of file +} diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt index 1206c99..7826702 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorApplicationTests.kt @@ -48,98 +48,95 @@ import org.testcontainers.junit.jupiter.Testcontainers @SpringBootTest @MockitoBean(types = [MtbFileSender::class]) @TestPropertySource( - properties = [ - "app.rest.uri=http://example.com", - "app.pseudonymize.generator=buildin", - "app.consent.service=none" - ] + properties = + [ + "app.rest.uri=http://example.com", + "app.pseudonymize.generator=buildin", + "app.consent.service=none", + ] ) class EtlProcessorApplicationTests : AbstractTestcontainerTest() { - @Test - fun contextLoadsIfMtbFileSenderConfigured(@Autowired context: ApplicationContext) { - // Simply check bean configuration - assertThat(context).isNotNull - } + @Test + fun contextLoadsIfMtbFileSenderConfigured(@Autowired context: ApplicationContext) { + // Simply check bean configuration + assertThat(context).isNotNull + } - @Nested - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) - @AutoConfigureMockMvc - @TestPropertySource( - properties = [ - "app.pseudonymize.generator=buildin", - "app.consent.service=none", - "app.transformations[0].path=diagnoses[*].code.version", - "app.transformations[0].from=2013", - "app.transformations[0].to=2014", - ] - ) - inner class TransformationTest { + @Nested + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) + @AutoConfigureMockMvc + @TestPropertySource( + properties = + [ + "app.pseudonymize.generator=buildin", + "app.consent.service=none", + "app.transformations[0].path=diagnoses[*].code.version", + "app.transformations[0].from=2013", + "app.transformations[0].to=2014", + ] + ) + inner class TransformationTest { - @MockitoBean - private lateinit var mtbFileSender: MtbFileSender + @MockitoBean private lateinit var mtbFileSender: MtbFileSender - @Autowired - private lateinit var mockMvc: MockMvc + @Autowired private lateinit var mockMvc: MockMvc - @Autowired - private lateinit var objectMapper: ObjectMapper + @Autowired private lateinit var objectMapper: ObjectMapper - @BeforeEach - fun setup(@Autowired requestRepository: RequestRepository) { - requestRepository.deleteAll() - } + @BeforeEach + fun setup(@Autowired requestRepository: RequestRepository) { + requestRepository.deleteAll() + } - @Test - fun mtbFileIsTransformed() { - doAnswer { - MtbFileSender.Response(RequestStatus.SUCCESS) - }.whenever(mtbFileSender).send(any<DnpmV2MtbFileRequest>()) + @Test + fun mtbFileIsTransformed() { + doAnswer { MtbFileSender.Response(RequestStatus.SUCCESS) } + .whenever(mtbFileSender) + .send(any<DnpmV2MtbFileRequest>()) - val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .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() - .id("1234") - .patient(Reference.builder().id("TEST_12345678").build()) - .code(Coding.builder().code("F79.9").version("2013").build()) - .build(), - ) - ) - .build() + val mtbFile = + Mtb.builder() + .patient(Patient.builder().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() + .id("1234") + .patient(Reference.builder().id("TEST_12345678").build()) + .code(Coding.builder().code("F79.9").version("2013").build()) + .build(), + ) + ) + .build() - mockMvc.post("/mtbfile") { - content = objectMapper.writeValueAsString(mtbFile) - contentType = MediaType.APPLICATION_JSON - }.andExpect { - status { - isAccepted() - } - } + mockMvc + .post("/mtbfile") { + content = objectMapper.writeValueAsString(mtbFile) + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isAccepted() } } - val captor = argumentCaptor<DnpmV2MtbFileRequest>() - verify(mtbFileSender).send(captor.capture()) - assertThat(captor.firstValue.content.diagnoses).hasSize(1).allMatch { diagnosis -> - diagnosis.code.version == "2014" - } - } + val captor = argumentCaptor<DnpmV2MtbFileRequest>() + verify(mtbFileSender).send(captor.capture()) + assertThat(captor.firstValue.content.diagnoses).hasSize(1).allMatch { diagnosis -> + diagnosis.code.version == "2014" + } } - + } } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt index 308d0cc..1ebf458 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt @@ -9,65 +9,79 @@ import org.junit.jupiter.api.Test import org.springframework.data.repository.Repository class EtlProcessorArchTest { - private lateinit var noTestClasses: JavaClasses @BeforeEach fun setUp() { - this.noTestClasses = ClassFileImporter() - .withImportOption { !(it.contains("/test/") || it.contains("/integrationTest/")) } - .importPackages("dev.dnpm.etl.processor") + this.noTestClasses = + ClassFileImporter() + .withImportOption { !(it.contains("/test/") || it.contains("/integrationTest/")) } + .importPackages("dev.dnpm.etl.processor") } @Test fun noClassesInInputPackageShouldDependOnMonitoringPackage() { - val rule = noClasses() - .that() - .resideInAPackage("..input") - .should().dependOnClassesThat() - .resideInAnyPackage("..monitoring") + val rule = + noClasses() + .that() + .resideInAPackage("..input") + .should() + .dependOnClassesThat() + .resideInAnyPackage("..monitoring") rule.check(noTestClasses) } @Test fun noClassesInInputPackageShouldDependOnRepositories() { - val rule = noClasses() - .that() - .resideInAPackage("..input") - .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository") + val rule = + noClasses() + .that() + .resideInAPackage("..input") + .should() + .dependOnClassesThat() + .haveSimpleNameEndingWith("Repository") rule.check(noTestClasses) } @Test fun noClassesInOutputPackageShouldDependOnRepositories() { - val rule = noClasses() - .that() - .resideInAPackage("..output") - .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository") + val rule = + noClasses() + .that() + .resideInAPackage("..output") + .should() + .dependOnClassesThat() + .haveSimpleNameEndingWith("Repository") rule.check(noTestClasses) } @Test fun noClassesInWebPackageShouldDependOnRepositories() { - val rule = noClasses() - .that() - .resideInAPackage("..web") - .should().dependOnClassesThat().haveSimpleNameEndingWith("Repository") + val rule = + noClasses() + .that() + .resideInAPackage("..web") + .should() + .dependOnClassesThat() + .haveSimpleNameEndingWith("Repository") rule.check(noTestClasses) } @Test fun repositoryClassNamesShouldEndWithRepository() { - val rule = classes() - .that() - .areInterfaces().and().areAssignableTo(Repository::class.java) - .should().haveSimpleNameEndingWith("Repository") + val rule = + classes() + .that() + .areInterfaces() + .and() + .areAssignableTo(Repository::class.java) + .should() + .haveSimpleNameEndingWith("Repository") rule.check(noTestClasses) } - -}
\ No newline at end of file +} diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt index 5e25428..f9fe2d4 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt @@ -50,127 +50,136 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean @SpringBootTest @ContextConfiguration( - classes = [ - AppConfiguration::class, - AppSecurityConfiguration::class, - KafkaAutoConfiguration::class, - AppKafkaConfiguration::class, - AppRestConfiguration::class, - ConsentEvaluator::class - ] + classes = + [ + AppConfiguration::class, + AppSecurityConfiguration::class, + KafkaAutoConfiguration::class, + AppKafkaConfiguration::class, + AppRestConfiguration::class, + ConsentEvaluator::class, + ], ) @MockitoBean(types = [ObjectMapper::class]) @TestPropertySource( - properties = [ - "app.pseudonymize.generator=BUILDIN", - ] + properties = + [ + "app.pseudonymize.generator=BUILDIN", + ], ) class AppConfigurationTest { - @Nested - @TestPropertySource( - properties = [ - "app.rest.uri=http://localhost:9000" - ] - ) - inner class AppConfigurationRestTest(private val context: ApplicationContext) { - + @TestPropertySource(properties = ["app.rest.uri=http://localhost:9000"]) + inner class AppConfigurationRestTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseRestMtbFileSenderNotKafkaMtbFileSender() { assertThat(context.getBean(RestMtbFileSender::class.java)).isNotNull - assertThrows<NoSuchBeanDefinitionException> { context.getBean(KafkaMtbFileSender::class.java) } + assertThrows<NoSuchBeanDefinitionException> { + context.getBean(KafkaMtbFileSender::class.java) + } } - } @Nested @TestPropertySource( - properties = [ - "app.kafka.servers=localhost:9092", - "app.kafka.output-topic=test", - "app.kafka.output-response-topic=test-response", - "app.kafka.group-id=test" - ] + properties = + [ + "app.kafka.servers=localhost:9092", + "app.kafka.output-topic=test", + "app.kafka.output-response-topic=test-response", + "app.kafka.group-id=test", + ], ) @MockitoBean(types = [RequestRepository::class]) - inner class AppConfigurationKafkaTest(private val context: ApplicationContext) { - + inner class AppConfigurationKafkaTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseKafkaMtbFileSenderNotRestMtbFileSender() { assertThrows<NoSuchBeanDefinitionException> { context.getBean(RestMtbFileSender::class.java) } assertThat(context.getBean(KafkaMtbFileSender::class.java)).isNotNull } - } @Nested @TestPropertySource( - properties = [ - "app.rest.uri=http://localhost:9000", - "app.kafka.servers=localhost:9092", - "app.kafka.output-topic=test", - "app.kafka.output-response-topic=test-response", - "app.kafka.group-id=test" - ] + properties = + [ + "app.rest.uri=http://localhost:9000", + "app.kafka.servers=localhost:9092", + "app.kafka.output-topic=test", + "app.kafka.output-response-topic=test-response", + "app.kafka.group-id=test", + ], ) - inner class AppConfigurationRestInPrecedenceTest(private val context: ApplicationContext) { - + inner class AppConfigurationRestInPrecedenceTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseRestMtbFileSenderNotKafkaMtbFileSender() { assertThat(context.getBean(RestMtbFileSender::class.java)).isNotNull - assertThrows<NoSuchBeanDefinitionException> { context.getBean(KafkaMtbFileSender::class.java) } + assertThrows<NoSuchBeanDefinitionException> { + context.getBean(KafkaMtbFileSender::class.java) + } } - } @Nested @TestPropertySource( - properties = [ - "app.kafka.servers=localhost:9092", - "app.kafka.output-topic=test", - "app.kafka.output-response-topic=test-response", - "app.kafka.group-id=test" - ] + properties = + [ + "app.kafka.servers=localhost:9092", + "app.kafka.output-topic=test", + "app.kafka.output-response-topic=test-response", + "app.kafka.group-id=test", + ], ) - inner class AppConfigurationWithoutKafkaInputTest(private val context: ApplicationContext) { - + inner class AppConfigurationWithoutKafkaInputTest( + private val context: ApplicationContext, + ) { @Test fun shouldNotUseKafkaInputListener() { - assertThrows<NoSuchBeanDefinitionException> { context.getBean(KafkaInputListener::class.java) } + assertThrows<NoSuchBeanDefinitionException> { + context.getBean(KafkaInputListener::class.java) + } } - } @Nested @TestPropertySource( - properties = [ - "app.kafka.servers=localhost:9092", - "app.kafka.input-topic=test_input", - "app.kafka.output-topic=test", - "app.kafka.output-response-topic=test-response", - "app.kafka.group-id=test" - ] + properties = + [ + "app.kafka.servers=localhost:9092", + "app.kafka.input-topic=test_input", + "app.kafka.output-topic=test", + "app.kafka.output-response-topic=test-response", + "app.kafka.group-id=test", + ], ) @MockitoBean(types = [RequestProcessor::class]) - inner class AppConfigurationUsingKafkaInputTest(private val context: ApplicationContext) { - + inner class AppConfigurationUsingKafkaInputTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseKafkaInputListener() { assertThat(context.getBean(KafkaInputListener::class.java)).isNotNull } - } @Nested @TestPropertySource( - properties = [ - "app.transformations[0].path=consent.status", - "app.transformations[0].from=rejected", - "app.transformations[0].to=accept", - ] + properties = + [ + "app.transformations[0].path=consent.status", + "app.transformations[0].from=rejected", + "app.transformations[0].to=accept", + ], ) - inner class AppConfigurationTransformationTest(private val context: ApplicationContext) { - + inner class AppConfigurationTransformationTest( + private val context: ApplicationContext, + ) { @Test fun shouldRecognizeTransformations() { val appConfigProperties = context.getBean(AppConfigProperties::class.java) @@ -178,109 +187,87 @@ class AppConfigurationTest { assertThat(appConfigProperties).isNotNull assertThat(appConfigProperties.transformations).hasSize(1) } - } @Nested inner class AppConfigurationPseudonymizeTest { - @Nested - @TestPropertySource( - properties = [ - "app.pseudonymize.generator=buildin" - ] - ) - inner class AppConfigurationPseudonymizeGeneratorBuildinTest(private val context: ApplicationContext) { - + @TestPropertySource(properties = ["app.pseudonymize.generator=buildin"]) + inner class AppConfigurationPseudonymizeGeneratorBuildinTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseConfiguredGenerator() { assertThat(context.getBean(AnonymizingGenerator::class.java)).isNotNull } - } @Nested @TestPropertySource( - properties = [ - "app.pseudonymize.generator=gpas", - "app.pseudonymize.gpas.uri=http://localhost/" - ] + properties = + ["app.pseudonymize.generator=gpas", "app.pseudonymize.gpas.uri=http://localhost/"], ) - inner class AppConfigurationPseudonymizeGeneratorGpasTest(private val context: ApplicationContext) { - + inner class AppConfigurationPseudonymizeGeneratorGpasTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseConfiguredGenerator() { assertThat(context.getBean(GpasPseudonymGenerator::class.java)).isNotNull } - } @Nested @TestPropertySource( - properties = [ - "app.pseudonymize.generator=gpas", - "app.pseudonymize.gpas.soap-endpoint=http://localhost/" - ] + properties = + [ + "app.pseudonymize.generator=gpas", + "app.pseudonymize.gpas.soap-endpoint=http://localhost/", + ], ) - inner class AppConfigurationPseudonymizeGeneratorGpasSoapTest(private val context: ApplicationContext) { - + inner class AppConfigurationPseudonymizeGeneratorGpasSoapTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseConfiguredGenerator() { assertThat(context.getBean(GpasSoapPseudonymGenerator::class.java)).isNotNull } - } @Nested - @TestPropertySource( - properties = [ - "app.security.enable-tokens=true" - ] - ) + @TestPropertySource(properties = ["app.security.enable-tokens=true"]) @MockitoBean( - types = [ - InMemoryUserDetailsManager::class, - PasswordEncoder::class, - TokenRepository::class - ] + types = [InMemoryUserDetailsManager::class, PasswordEncoder::class, TokenRepository::class], ) - inner class AppConfigurationTokenEnabledTest(private val context: ApplicationContext) { - + inner class AppConfigurationTokenEnabledTest( + private val context: ApplicationContext, + ) { @Test fun checkTokenService() { assertThat(context.getBean(TokenService::class.java)).isNotNull } - } @Nested @MockitoBean( - types = [ - InMemoryUserDetailsManager::class, - PasswordEncoder::class, - TokenRepository::class - ] + types = [InMemoryUserDetailsManager::class, PasswordEncoder::class, TokenRepository::class], ) - inner class AppConfigurationTokenDisabledTest(private val context: ApplicationContext) { - + inner class AppConfigurationTokenDisabledTest( + private val context: ApplicationContext, + ) { @Test fun checkTokenService() { assertThrows<NoSuchBeanDefinitionException> { context.getBean(TokenService::class.java) } } - } - } @Nested @TestPropertySource( - properties = [ - "app.rest.uri=http://localhost:9000", - "app.max-retry-attempts=5" - ] + properties = ["app.rest.uri=http://localhost:9000", "app.max-retry-attempts=5"], ) - inner class AppConfigurationRetryTest(private val context: ApplicationContext) { - + inner class AppConfigurationRetryTest( + private val context: ApplicationContext, + ) { private val maxRetryAttempts = 5 @Test @@ -295,33 +282,32 @@ class AppConfigurationTest { } } } - } @Nested @TestPropertySource( - properties = [ - "app.consent.service=GICS", - "app.consent.gics.uri=http://localhost:9000", - ] + properties = + [ + "app.consent.service=GICS", + "app.consent.gics.uri=http://localhost:9000", + ], ) - inner class AppConfigurationConsentGicsTest(private val context: ApplicationContext) { - + inner class AppConfigurationConsentGicsTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseConfiguredGenerator() { assertThat(context.getBean(GicsConsentService::class.java)).isNotNull } - } @Nested - inner class AppConfigurationConsentBuildinTest(private val context: ApplicationContext) { - + inner class AppConfigurationConsentBuildinTest( + private val context: ApplicationContext, + ) { @Test fun shouldUseConfiguredGenerator() { assertThat(context.getBean(MtbFileConsentService::class.java)).isNotNull } - } - } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/GPasConfigPropertiesTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/GPasConfigPropertiesTest.kt index 66e0660..bda5b79 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/GPasConfigPropertiesTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/GPasConfigPropertiesTest.kt @@ -7,17 +7,16 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.test.context.ContextConfiguration - @SpringBootTest @ContextConfiguration( - classes = [ - GPasConfigProperties::class, - ] + classes = + [ + GPasConfigProperties::class, + ], ) class GPasConfigPropertiesTest { - @EnableConfigurationProperties(GPasConfigProperties::class) - class TestConfig {} + class TestConfig @Test fun shouldUseConfiguredPatientDomainIfPidDomainGiven() { @@ -25,9 +24,8 @@ class GPasConfigPropertiesTest { .withUserConfiguration(TestConfig::class.java) .withPropertyValues( "app.pseudonymize.gpas.uri=http://localhost/", - "app.pseudonymize.gpas.pid-domain=test-pid-domain" - ) - .run { context -> + "app.pseudonymize.gpas.pid-domain=test-pid-domain", + ).run { context -> val properties = context.getBean(GPasConfigProperties::class.java) assertThat(properties).isNotNull @@ -41,14 +39,12 @@ class GPasConfigPropertiesTest { .withUserConfiguration(TestConfig::class.java) .withPropertyValues( "app.pseudonymize.gpas.uri=http://localhost/", - "app.pseudonymize.gpas.patient-domain=test-patient-domain" - ) - .run { context -> + "app.pseudonymize.gpas.patient-domain=test-patient-domain", + ).run { context -> val properties = context.getBean(GPasConfigProperties::class.java) assertThat(properties).isNotNull assertThat(properties.patientDomain).isEqualTo("test-patient-domain") } } - -}
\ No newline at end of file +} diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt index 6ca420f..5ee6922 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/helpers.kt @@ -23,8 +23,7 @@ import org.mockito.ArgumentMatchers @Suppress("UNCHECKED_CAST") inline fun <reified T> anyValueClass(): T { - val unboxedClass = T::class.java.declaredFields.first().type - return ArgumentMatchers.any(unboxedClass as Class<T>) - ?: T::class.java.getDeclaredMethod("box-impl", unboxedClass) - .invoke(null, null) as T -}
\ No newline at end of file + val unboxedClass = T::class.java.declaredFields.first().type + return ArgumentMatchers.any(unboxedClass as Class<T>) + ?: T::class.java.getDeclaredMethod("box-impl", unboxedClass).invoke(null, null) as T +} 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 5f2d7ca..e966898 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -29,6 +29,8 @@ import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.* +import java.time.Instant +import java.util.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -48,180 +50,177 @@ import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.post -import java.time.Instant -import java.util.* @WebMvcTest(controllers = [MtbFileRestController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( - classes = [ - MtbFileRestController::class, - AppSecurityConfiguration::class, - MtbFileConsentService::class - ] + classes = + [ + MtbFileRestController::class, + AppSecurityConfiguration::class, + MtbFileConsentService::class, + ] ) -@MockitoBean(types = [TokenRepository::class, RequestProcessor::class, ConsentEvaluator::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" - ] + properties = + [ + "app.pseudonymize.generator=BUILDIN", + "app.security.admin-user=admin", + "app.security.admin-password={noop}very-secret", + "app.security.enable-tokens=true", + ] ) class MtbFileRestControllerTest { - lateinit var mockMvc: MockMvc - lateinit var requestProcessor: RequestProcessor - lateinit var consentEvaluator: ConsentEvaluator - - @BeforeEach - fun setup( - @Autowired mockMvc: MockMvc, - @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 - fun testShouldGrantPermissionToSendMtbFile() { - mockMvc.post("/mtbfile") { - with(user("onkostarserver").roles("MTBFILE")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - }.andExpect { - status { isAccepted() } + lateinit var mockMvc: MockMvc + lateinit var requestProcessor: RequestProcessor + lateinit var consentEvaluator: ConsentEvaluator + + @BeforeEach + fun setup( + @Autowired mockMvc: MockMvc, + @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 + fun testShouldGrantPermissionToSendMtbFile() { + mockMvc + .post("/mtbfile") { + with(user("onkostarserver").roles("MTBFILE")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) } - - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } - + .andExpect { status { isAccepted() } } + + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } + + @Test + fun testShouldGrantPermissionToSendMtbFileToAdminUser() { + mockMvc + .post("/mtbfile") { + with(user("onkostarserver").roles("ADMIN")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isAccepted() } } + + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } + + @Test + fun testShouldDenyPermissionToSendMtbFile() { + mockMvc + .post("/mtbfile") { + with(anonymous()) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isUnauthorized() } } + + verify(requestProcessor, never()).processMtbFile(any<Mtb>()) + } + + @Test + fun testShouldDenyPermissionToSendMtbFileForUser() { + mockMvc + .post("/mtbfile") { + with(user("fakeuser").roles("USER")) + contentType = MediaType.APPLICATION_JSON + content = ObjectMapper().writeValueAsString(mtbFile) + } + .andExpect { status { isForbidden() } } + + verify(requestProcessor, never()).processMtbFile(any<Mtb>()) + } + + @Test + fun testShouldGrantPermissionToDeletePatientData() { + mockMvc + .delete("/mtbfile/12345678") { with(user("onkostarserver").roles("MTBFILE")) } + .andExpect { status { isAccepted() } } + + verify(requestProcessor, times(1)) + .processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)) + } + + @Test + fun testShouldDenyPermissionToDeletePatientData() { + mockMvc + .delete("/mtbfile/12345678") { with(anonymous()) } + .andExpect { status { isUnauthorized() } } + + verify(requestProcessor, never()).processDeletion(anyValueClass(), any()) + } + + @Nested + @MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::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.security.enable-oidc=true", + ] + ) + inner class WithOidcEnabled { @Test fun testShouldGrantPermissionToSendMtbFileToAdminUser() { - mockMvc.post("/mtbfile") { + mockMvc + .post("/mtbfile") { with(user("onkostarserver").roles("ADMIN")) contentType = MediaType.APPLICATION_JSON content = ObjectMapper().writeValueAsString(mtbFile) - }.andExpect { - status { isAccepted() } - } - - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } - - @Test - fun testShouldDenyPermissionToSendMtbFile() { - mockMvc.post("/mtbfile") { - with(anonymous()) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - }.andExpect { - status { isUnauthorized() } - } + } + .andExpect { status { isAccepted() } } - verify(requestProcessor, never()).processMtbFile(any<Mtb>()) + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) } @Test - fun testShouldDenyPermissionToSendMtbFileForUser() { - mockMvc.post("/mtbfile") { - with(user("fakeuser").roles("USER")) + fun testShouldGrantPermissionToSendMtbFileToUser() { + mockMvc + .post("/mtbfile") { + with(user("onkostarserver").roles("USER")) contentType = MediaType.APPLICATION_JSON content = ObjectMapper().writeValueAsString(mtbFile) - }.andExpect { - status { isForbidden() } - } - - verify(requestProcessor, never()).processMtbFile(any<Mtb>()) - } - - @Test - fun testShouldGrantPermissionToDeletePatientData() { - mockMvc.delete("/mtbfile/12345678") { - with(user("onkostarserver").roles("MTBFILE")) - }.andExpect { - status { isAccepted() } - } + } + .andExpect { status { isAccepted() } } - verify(requestProcessor, times(1)).processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)) + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) } + } - @Test - fun testShouldDenyPermissionToDeletePatientData() { - mockMvc.delete("/mtbfile/12345678") { - with(anonymous()) - }.andExpect { - status { isUnauthorized() } - } + companion object { - verify(requestProcessor, never()).processDeletion(anyValueClass(), any()) - } - - @Nested - @MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::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.security.enable-oidc=true" - ] - ) - inner class WithOidcEnabled { - @Test - fun testShouldGrantPermissionToSendMtbFileToAdminUser() { - mockMvc.post("/mtbfile") { - with(user("onkostarserver").roles("ADMIN")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - }.andExpect { - status { isAccepted() } - } - - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } - - @Test - fun testShouldGrantPermissionToSendMtbFileToUser() { - mockMvc.post("/mtbfile") { - with(user("onkostarserver").roles("USER")) - contentType = MediaType.APPLICATION_JSON - content = ObjectMapper().writeValueAsString(mtbFile) - }.andExpect { - status { isAccepted() } - } - - verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) - } - } - - companion object { - - val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .id("PID") - .build() - ) + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("PID").build()) .episodesOfCare( listOf( MtbEpisodeOfCare.builder() .id("1") .patient(Reference.builder().id("PID").build()) - .period(PeriodDate.builder().start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))).build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))) + .build() + ) .build() ) ) .build() - - } - + } } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt index 428bca9..428a99d 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt @@ -21,6 +21,7 @@ package dev.dnpm.etl.processor.monitoring import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.output.MtbFileSender +import java.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -32,7 +33,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.transaction.annotation.Transactional import org.testcontainers.junit.jupiter.Testcontainers -import java.time.Instant @Testcontainers @ExtendWith(SpringExtension::class) @@ -41,35 +41,30 @@ import java.time.Instant @Transactional @MockitoBean(types = [MtbFileSender::class]) @TestPropertySource( - properties = [ - "app.pseudonymize.generator=buildin", - "app.rest.uri=http://example.com" - ] + properties = ["app.pseudonymize.generator=buildin", "app.rest.uri=http://example.com"] ) class RequestRepositoryTest : AbstractTestcontainerTest() { - private lateinit var requestRepository: RequestRepository + private lateinit var requestRepository: RequestRepository - @BeforeEach - fun setUp( - @Autowired requestRepository: RequestRepository - ) { - this.requestRepository = requestRepository - } + @BeforeEach + fun setUp(@Autowired requestRepository: RequestRepository) { + this.requestRepository = requestRepository + } - @Test - fun shouldSaveRequest() { - val request = Request( + @Test + fun shouldSaveRequest() { + val request = + Request( randomRequestId(), PatientPseudonym("TEST_12345678901"), PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, RequestStatus.WARNING, - Instant.parse("2023-07-07T00:00:00Z") + Instant.parse("2023-07-07T00:00:00Z"), ) - requestRepository.save(request) - } - -}
\ No newline at end of file + requestRepository.save(request) + } +} 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 578810e..cccd614 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt @@ -41,7 +41,6 @@ import java.io.IOException import java.net.URI class GpasPseudonymGeneratorTest { - private lateinit var mockRestServiceServer: MockRestServiceServer private lateinit var generator: GpasPseudonymGenerator private lateinit var restTemplate: RestTemplate @@ -50,14 +49,8 @@ class GpasPseudonymGeneratorTest { @BeforeEach fun setup() { val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() - val gPasConfigProperties = GPasConfigProperties( - CONFIGURED_URI, - null, - null, - "test", "test2", - null, - null - ) + val gPasConfigProperties = + GPasConfigProperties(CONFIGURED_URI, null, null, "test", "test2", null, null) this.restTemplate = RestTemplate() this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) @@ -71,13 +64,8 @@ class GpasPseudonymGeneratorTest { .expect(method(HttpMethod.POST)) .andExpect(requestTo(EXPECTED_URI)) .andRespond { - withStatus(HttpStatus.OK).body( - getDummyResponseBody( - "1234", - "test", - "test1234ABCDEF567890" - ) - ) + withStatus(HttpStatus.OK) + .body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890")) .createResponse(it) } @@ -89,9 +77,7 @@ class GpasPseudonymGeneratorTest { this.mockRestServiceServer .expect(method(HttpMethod.POST)) .andExpect(requestTo(EXPECTED_URI)) - .andRespond { - withException(IOException("Simulated IO error")).createResponse(it) - } + .andRespond { withException(IOException("Simulated IO error")).createResponse(it) } assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") } } @@ -105,52 +91,56 @@ class GpasPseudonymGeneratorTest { withStatus(HttpStatus.FOUND) .header( HttpHeaders.LOCATION, - $$"https://localhost/ttp-fhir/fhir/gpas/$pseudonymizeAllowCreate" - ) - .createResponse(it) + $$"https://localhost/ttp-fhir/fhir/gpas/$pseudonymizeAllowCreate", + ).createResponse(it) } assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") } } companion object { - const val CONFIGURED_URI = "https://localhost/ttp-fhir/fhir/gpas" - val EXPECTED_URI = URIBuilder(URI.create(CONFIGURED_URI)).appendPath($$"$pseudonymizeAllowCreate") - .build()!! + val EXPECTED_URI = + URIBuilder(URI.create(CONFIGURED_URI)).appendPath($$"$pseudonymizeAllowCreate").build()!! - fun getDummyResponseBody(original: String, target: String, pseudonym: String) = """{ - "resourceType": "Parameters", - "parameter": [ + fun getDummyResponseBody( + original: String, + target: String, + pseudonym: String, + ) = """ { - "name": "pseudonym", - "part": [ - { - "name": "original", - "valueIdentifier": { - "system": "https://ths-greifswald.de/gpas", - "value": "$original" - } - }, - { - "name": "target", - "valueIdentifier": { - "system": "https://ths-greifswald.de/gpas", - "value": "$target" - } - }, + "resourceType": "Parameters", + "parameter": [ { "name": "pseudonym", - "valueIdentifier": { - "system": "https://ths-greifswald.de/gpas", - "value": "$pseudonym" - } + "part": [ + { + "name": "original", + "valueIdentifier": { + "system": "https://ths-greifswald.de/gpas", + "value": "$original" + } + }, + { + "name": "target", + "valueIdentifier": { + "system": "https://ths-greifswald.de/gpas", + "value": "$target" + } + }, + { + "name": "pseudonym", + "valueIdentifier": { + "system": "https://ths-greifswald.de/gpas", + "value": "$pseudonym" + } + } + ] } ] } - ] - }""".trimIndent() - + + """.trimIndent() } } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt index 9fcdc16..d9489f2 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt @@ -25,6 +25,7 @@ import dev.dnpm.etl.processor.monitoring.RequestRepository import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType import dev.dnpm.etl.processor.output.MtbFileSender +import java.time.Instant import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -36,7 +37,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.transaction.annotation.Transactional import org.testcontainers.junit.jupiter.Testcontainers -import java.time.Instant @Testcontainers @ExtendWith(SpringExtension::class) @@ -44,101 +44,95 @@ import java.time.Instant @Transactional @MockitoBean(types = [MtbFileSender::class]) @TestPropertySource( - properties = [ - "app.pseudonymize.generator=buildin", - "app.rest.uri=http://example.com" - ] + properties = ["app.pseudonymize.generator=buildin", "app.rest.uri=http://example.com"] ) class RequestServiceIntegrationTest : AbstractTestcontainerTest() { - private lateinit var requestRepository: RequestRepository - - private lateinit var requestService: RequestService - - @BeforeEach - fun setup( - @Autowired requestRepository: RequestRepository - ) { - this.requestRepository = requestRepository - this.requestService = RequestService(requestRepository) - } - - @Test - fun shouldResultInEmptyRequestList() { - val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM) - - assertThat(actual).isEmpty() - } - - private fun setupTestData() { - // Prepare DB - this.requestRepository.saveAll( - listOf( - Request( - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("0123456789abcdef1"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS, - Instant.parse("2023-07-07T02:00:00Z") - ), - // Should be ignored - wrong patient ID --> - Request( - randomRequestId(), - PatientPseudonym("TEST_12345678902"), - PatientId("P2"), - Fingerprint("0123456789abcdef2"), - RequestType.MTB_FILE, - RequestStatus.WARNING, - Instant.parse("2023-08-08T00:00:00Z") - ), - // <-- - Request( - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P2"), - Fingerprint("0123456789abcdee1"), - RequestType.DELETE, - RequestStatus.SUCCESS, - Instant.parse("2023-08-08T02:00:00Z") - ) - ) + private lateinit var requestRepository: RequestRepository + + private lateinit var requestService: RequestService + + @BeforeEach + fun setup(@Autowired requestRepository: RequestRepository) { + this.requestRepository = requestRepository + this.requestService = RequestService(requestRepository) + } + + @Test + fun shouldResultInEmptyRequestList() { + val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM) + + assertThat(actual).isEmpty() + } + + private fun setupTestData() { + // Prepare DB + this.requestRepository.saveAll( + listOf( + Request( + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("0123456789abcdef1"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + Instant.parse("2023-07-07T02:00:00Z"), + ), + // Should be ignored - wrong patient ID --> + Request( + randomRequestId(), + PatientPseudonym("TEST_12345678902"), + PatientId("P2"), + Fingerprint("0123456789abcdef2"), + RequestType.MTB_FILE, + RequestStatus.WARNING, + Instant.parse("2023-08-08T00:00:00Z"), + ), + // <-- + Request( + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P2"), + Fingerprint("0123456789abcdee1"), + RequestType.DELETE, + RequestStatus.SUCCESS, + Instant.parse("2023-08-08T02:00:00Z"), + ), ) - } - - @Test - fun shouldResultInSortedRequestList() { - setupTestData() + ) + } - val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM) + @Test + fun shouldResultInSortedRequestList() { + setupTestData() - assertThat(actual).hasSize(2) - assertThat(actual[0].fingerprint).isEqualTo(Fingerprint("0123456789abcdee1")) - assertThat(actual[1].fingerprint).isEqualTo(Fingerprint("0123456789abcdef1")) - } + val actual = requestService.allRequestsByPatientPseudonym(TEST_PATIENT_PSEUDONYM) - @Test - fun shouldReturnDeleteRequestAsLastRequest() { - setupTestData() + assertThat(actual).hasSize(2) + assertThat(actual[0].fingerprint).isEqualTo(Fingerprint("0123456789abcdee1")) + assertThat(actual[1].fingerprint).isEqualTo(Fingerprint("0123456789abcdef1")) + } - val actual = requestService.isLastRequestWithKnownStatusDeletion(TEST_PATIENT_PSEUDONYM) + @Test + fun shouldReturnDeleteRequestAsLastRequest() { + setupTestData() - assertThat(actual).isTrue() - } + val actual = requestService.isLastRequestWithKnownStatusDeletion(TEST_PATIENT_PSEUDONYM) - @Test - fun shouldReturnLastMtbFileRequest() { - setupTestData() + assertThat(actual).isTrue() + } - val actual = requestService.lastMtbFileRequestForPatientPseudonym(TEST_PATIENT_PSEUDONYM) + @Test + fun shouldReturnLastMtbFileRequest() { + setupTestData() - assertThat(actual).isNotNull - assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef1")) - } + val actual = requestService.lastMtbFileRequestForPatientPseudonym(TEST_PATIENT_PSEUDONYM) - companion object { - val TEST_PATIENT_PSEUDONYM = PatientPseudonym("TEST_12345678901") - } + assertThat(actual).isNotNull + assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef1")) + } -}
\ No newline at end of file + companion object { + val TEST_PATIENT_PSEUDONYM = PatientPseudonym("TEST_12345678901") + } +} diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt index 8e5d38e..c081319 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt @@ -32,6 +32,7 @@ import dev.dnpm.etl.processor.security.TokenService import dev.dnpm.etl.processor.security.UserRoleService import dev.dnpm.etl.processor.services.RequestProcessor import dev.dnpm.etl.processor.services.TransformationService +import java.time.Instant import org.assertj.core.api.Assertions.assertThat import org.htmlunit.WebClient import org.htmlunit.html.HtmlPage @@ -64,320 +65,288 @@ import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder import org.springframework.web.context.WebApplicationContext import reactor.core.publisher.Sinks import reactor.test.StepVerifier -import java.time.Instant abstract class MockSink : Sinks.Many<Boolean> @WebMvcTest(controllers = [ConfigController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( - classes = [ - ConfigController::class, - AppConfiguration::class, - AppSecurityConfiguration::class - ] -) -@TestPropertySource( - properties = [ - "app.pseudonymize.generator=BUILDIN" - ] + classes = [ConfigController::class, AppConfiguration::class, AppSecurityConfiguration::class] ) +@TestPropertySource(properties = ["app.pseudonymize.generator=BUILDIN"]) @MockitoBean(name = "configsUpdateProducer", types = [MockSink::class]) @MockitoBean( - types = [ - Generator::class, - MtbFileSender::class, - RequestProcessor::class, - TransformationService::class, - GPasConnectionCheckService::class, - RestConnectionCheckService::class, - GIcsConnectionCheckService::class - ] + types = + [ + Generator::class, + MtbFileSender::class, + RequestProcessor::class, + TransformationService::class, + GPasConnectionCheckService::class, + RestConnectionCheckService::class, + GIcsConnectionCheckService::class, + ] ) class ConfigControllerTest { - private lateinit var mockMvc: MockMvc - private lateinit var webClient: WebClient + private lateinit var mockMvc: MockMvc + private lateinit var webClient: WebClient + + private lateinit var requestProcessor: RequestProcessor + private lateinit var connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + + @BeforeEach + fun setup( + @Autowired mockMvc: MockMvc, + @Autowired requestProcessor: RequestProcessor, + @Autowired connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ) { + this.mockMvc = mockMvc + this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build() + this.requestProcessor = requestProcessor + this.connectionCheckUpdateProducer = connectionCheckUpdateProducer + + webClient.options.isThrowExceptionOnScriptError = false + } + + @Test + fun testShouldRequestConfigPageIfLoggedIn() { + mockMvc + .get("/configs") { + with(user("admin").roles("ADMIN")) + accept(MediaType.TEXT_HTML) + } + .andExpect { + status { isOk() } + view { name("configs") } + } + } + + @Test + fun testShouldRedirectToLoginPageIfNotLoggedIn() { + mockMvc + .get("/configs") { + with(anonymous()) + accept(MediaType.TEXT_HTML) + } + .andExpect { + status { isFound() } + header { stringValues(HttpHeaders.LOCATION, "http://localhost/login") } + } + } - private lateinit var requestProcessor: RequestProcessor - private lateinit var connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + @Nested + @TestPropertySource( + properties = ["app.security.enable-tokens=true", "app.security.admin-user=admin"] + ) + @MockitoBean(types = [TokenService::class]) + inner class WithTokensEnabled { + private lateinit var tokenService: TokenService @BeforeEach - fun setup( - @Autowired mockMvc: MockMvc, - @Autowired requestProcessor: RequestProcessor, - @Autowired connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ) { - this.mockMvc = mockMvc - this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build() - this.requestProcessor = requestProcessor - this.connectionCheckUpdateProducer = connectionCheckUpdateProducer - - webClient.options.isThrowExceptionOnScriptError = false + fun setup(@Autowired tokenService: TokenService) { + webClient.options.isThrowExceptionOnScriptError = false + + this.tokenService = tokenService } @Test - fun testShouldRequestConfigPageIfLoggedIn() { - mockMvc.get("/configs") { + fun testShouldSaveNewToken() { + mockMvc + .post("/configs/tokens") { with(user("admin").roles("ADMIN")) accept(MediaType.TEXT_HTML) - }.andExpect { - status { isOk() } - view { name("configs") } - } + contentType = MediaType.APPLICATION_FORM_URLENCODED + content = "name=Testtoken" + } + .andExpect { + status { is2xxSuccessful() } + view { name("configs/tokens") } + } + + val captor = argumentCaptor<String>() + verify(tokenService, times(1)).addToken(captor.capture()) + + assertThat(captor.firstValue).isEqualTo("Testtoken") } @Test - fun testShouldRedirectToLoginPageIfNotLoggedIn() { - mockMvc.get("/configs") { - with(anonymous()) + fun testShouldNotSaveTokenWithExstingName() { + whenever(tokenService.addToken(anyString())) + .thenReturn(Result.failure(RuntimeException("Testfailure"))) + + mockMvc + .post("/configs/tokens") { + with(user("admin").roles("ADMIN")) accept(MediaType.TEXT_HTML) - }.andExpect { - status { isFound() } - header { - stringValues(HttpHeaders.LOCATION, "http://localhost/login") - } - } + contentType = MediaType.APPLICATION_FORM_URLENCODED + content = "name=Testtoken" + } + .andExpect { + status { is2xxSuccessful() } + view { name("configs/tokens") } + } + + val captor = argumentCaptor<String>() + verify(tokenService, times(1)).addToken(captor.capture()) + + assertThat(captor.firstValue).isEqualTo("Testtoken") } - @Nested - @TestPropertySource( - properties = [ - "app.security.enable-tokens=true", - "app.security.admin-user=admin" - ] - ) - @MockitoBean( - types = [ - TokenService::class - ] - ) - inner class WithTokensEnabled { - private lateinit var tokenService: TokenService - - @BeforeEach - fun setup( - @Autowired tokenService: TokenService - ) { - webClient.options.isThrowExceptionOnScriptError = false - - this.tokenService = tokenService - } - - @Test - fun testShouldSaveNewToken() { - mockMvc.post("/configs/tokens") { - with(user("admin").roles("ADMIN")) - accept(MediaType.TEXT_HTML) - contentType = MediaType.APPLICATION_FORM_URLENCODED - content = "name=Testtoken" - }.andExpect { - status { is2xxSuccessful() } - view { name("configs/tokens") } - } - - val captor = argumentCaptor<String>() - verify(tokenService, times(1)).addToken(captor.capture()) - - assertThat(captor.firstValue).isEqualTo("Testtoken") - } - - @Test - fun testShouldNotSaveTokenWithExstingName() { - whenever(tokenService.addToken(anyString())).thenReturn( - Result.failure( - RuntimeException( - "Testfailure" - ) - ) - ) - - mockMvc.post("/configs/tokens") { - with(user("admin").roles("ADMIN")) - accept(MediaType.TEXT_HTML) - contentType = MediaType.APPLICATION_FORM_URLENCODED - content = "name=Testtoken" - }.andExpect { - status { is2xxSuccessful() } - view { name("configs/tokens") } - } - - val captor = argumentCaptor<String>() - verify(tokenService, times(1)).addToken(captor.capture()) - - assertThat(captor.firstValue).isEqualTo("Testtoken") - } - - @Test - fun testShouldDeleteToken() { - mockMvc.delete("/configs/tokens/42") { - with(user("admin").roles("ADMIN")) - accept(MediaType.TEXT_HTML) - }.andExpect { - status { is2xxSuccessful() } - view { name("configs/tokens") } - } - - val captor = argumentCaptor<Long>() - verify(tokenService, times(1)).deleteToken(captor.capture()) + @Test + fun testShouldDeleteToken() { + mockMvc + .delete("/configs/tokens/42") { + with(user("admin").roles("ADMIN")) + accept(MediaType.TEXT_HTML) + } + .andExpect { + status { is2xxSuccessful() } + view { name("configs/tokens") } + } - assertThat(captor.firstValue).isEqualTo(42) - } + val captor = argumentCaptor<Long>() + verify(tokenService, times(1)).deleteToken(captor.capture()) - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldRenderConfigPageWithTokens() { - val page = webClient.getPage<HtmlPage>("http://localhost/configs") - assertThat( - page.getElementById("tokens") - ).isNotNull - } + assertThat(captor.firstValue).isEqualTo(42) } - @Nested - @TestPropertySource( - properties = [ - "app.security.enable-tokens=false" - ] - ) - inner class WithTokensDisabled { - @BeforeEach - fun setup() { - webClient.options.isThrowExceptionOnScriptError = false - } - - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldRenderConfigPageWithoutTokens() { - val page = webClient.getPage<HtmlPage>("http://localhost/configs") - assertThat( - page.getElementById("tokens") - ).isNull() - } + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldRenderConfigPageWithTokens() { + val page = webClient.getPage<HtmlPage>("http://localhost/configs") + assertThat(page.getElementById("tokens")).isNotNull } + } - @Nested - @TestPropertySource( - properties = [ - "app.security.enable-tokens=false", - "app.security.admin-user=admin", - "app.security.admin-password={noop}very-secret" - ] - ) - @MockitoBean( - types = [ - UserRoleService::class - ] - ) - inner class WithUserRolesEnabled { - private lateinit var userRoleService: UserRoleService + @Nested + @TestPropertySource(properties = ["app.security.enable-tokens=false"]) + inner class WithTokensDisabled { + @BeforeEach + fun setup() { + webClient.options.isThrowExceptionOnScriptError = false + } - @BeforeEach - fun setup( - @Autowired userRoleService: UserRoleService - ) { - webClient.options.isThrowExceptionOnScriptError = false + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldRenderConfigPageWithoutTokens() { + val page = webClient.getPage<HtmlPage>("http://localhost/configs") + assertThat(page.getElementById("tokens")).isNull() + } + } + + @Nested + @TestPropertySource( + properties = + [ + "app.security.enable-tokens=false", + "app.security.admin-user=admin", + "app.security.admin-password={noop}very-secret", + ] + ) + @MockitoBean(types = [UserRoleService::class]) + inner class WithUserRolesEnabled { + private lateinit var userRoleService: UserRoleService - this.userRoleService = userRoleService - } + @BeforeEach + fun setup(@Autowired userRoleService: UserRoleService) { + webClient.options.isThrowExceptionOnScriptError = false - @Test - fun testShouldDeleteUserRole() { - mockMvc.delete("/configs/userroles/42") { - with(user("admin").roles("ADMIN")) - accept(MediaType.TEXT_HTML) - }.andExpect { - status { is2xxSuccessful() } - view { name("configs/userroles") } - } + this.userRoleService = userRoleService + } - val captor = argumentCaptor<Long>() - verify(userRoleService, times(1)).deleteUserRole(captor.capture()) + @Test + fun testShouldDeleteUserRole() { + mockMvc + .delete("/configs/userroles/42") { + with(user("admin").roles("ADMIN")) + accept(MediaType.TEXT_HTML) + } + .andExpect { + status { is2xxSuccessful() } + view { name("configs/userroles") } + } - assertThat(captor.firstValue).isEqualTo(42) - } + val captor = argumentCaptor<Long>() + verify(userRoleService, times(1)).deleteUserRole(captor.capture()) - @Test - fun testShouldUpdateUserRole() { - mockMvc.put("/configs/userroles/42") { - with(user("admin").roles("ADMIN")) - accept(MediaType.TEXT_HTML) - contentType = MediaType.APPLICATION_FORM_URLENCODED - content = "role=ADMIN" - }.andExpect { - status { is2xxSuccessful() } - view { name("configs/userroles") } - } - - val idCaptor = argumentCaptor<Long>() - val roleCaptor = argumentCaptor<Role>() - verify(userRoleService, times(1)).updateUserRole( - idCaptor.capture(), - roleCaptor.capture() - ) - - assertThat(idCaptor.firstValue).isEqualTo(42) - assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN) - } + assertThat(captor.firstValue).isEqualTo(42) + } - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldRenderConfigPageWithUserRoles() { - val page = webClient.getPage<HtmlPage>("http://localhost/configs") - assertThat( - page.getElementById("userroles") - ).isNotNull - } + @Test + fun testShouldUpdateUserRole() { + mockMvc + .put("/configs/userroles/42") { + with(user("admin").roles("ADMIN")) + accept(MediaType.TEXT_HTML) + contentType = MediaType.APPLICATION_FORM_URLENCODED + content = "role=ADMIN" + } + .andExpect { + status { is2xxSuccessful() } + view { name("configs/userroles") } + } + + val idCaptor = argumentCaptor<Long>() + val roleCaptor = argumentCaptor<Role>() + verify(userRoleService, times(1)).updateUserRole(idCaptor.capture(), roleCaptor.capture()) + + assertThat(idCaptor.firstValue).isEqualTo(42) + assertThat(roleCaptor.firstValue).isEqualTo(Role.ADMIN) } - @Nested - inner class WithUserRolesDisabled { - @BeforeEach - fun setup() { - webClient.options.isThrowExceptionOnScriptError = false - } + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldRenderConfigPageWithUserRoles() { + val page = webClient.getPage<HtmlPage>("http://localhost/configs") + assertThat(page.getElementById("userroles")).isNotNull + } + } - @Test - fun testShouldRenderConfigPageWithoutUserRoles() { - val page = webClient.getPage<HtmlPage>("http://localhost/configs") - assertThat( - page.getElementById("userroles") - ).isNull() - } + @Nested + inner class WithUserRolesDisabled { + @BeforeEach + fun setup() { + webClient.options.isThrowExceptionOnScriptError = false } - @Nested - inner class SseTest { - private lateinit var webClient: WebTestClient + @Test + fun testShouldRenderConfigPageWithoutUserRoles() { + val page = webClient.getPage<HtmlPage>("http://localhost/configs") + assertThat(page.getElementById("userroles")).isNull() + } + } - @BeforeEach - fun setup( - applicationContext: WebApplicationContext - ) { - this.webClient = MockMvcWebTestClient - .bindToApplicationContext(applicationContext).build() - } + @Nested + inner class SseTest { + private lateinit var webClient: WebTestClient - @Test - fun testShouldRequestGPasSSE() { - val expectedEvent = - ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now()) - - connectionCheckUpdateProducer.tryEmitNext(expectedEvent) - connectionCheckUpdateProducer.emitComplete { _, _ -> true } - - val result = - webClient.get().uri("http://localhost/configs/events").accept(TEXT_EVENT_STREAM) - .exchange() - .expectStatus().isOk() - .expectHeader().contentType(TEXT_EVENT_STREAM) - .returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java) - - StepVerifier.create(result.responseBody) - .expectNext(expectedEvent) - .expectComplete() - .verify() - } + @BeforeEach + fun setup(applicationContext: WebApplicationContext) { + this.webClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext).build() } + @Test + fun testShouldRequestGPasSSE() { + val expectedEvent = + ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now()) + + connectionCheckUpdateProducer.tryEmitNext(expectedEvent) + connectionCheckUpdateProducer.emitComplete { _, _ -> true } + + val result = + webClient + .get() + .uri("http://localhost/configs/events") + .accept(TEXT_EVENT_STREAM) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(TEXT_EVENT_STREAM) + .returnResult(ConnectionCheckResult.GPasConnectionCheckResult::class.java) + + StepVerifier.create(result.responseBody).expectNext(expectedEvent).expectComplete().verify() + } + } } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt index 628112c..33fc9d2 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt @@ -27,6 +27,9 @@ import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType import dev.dnpm.etl.processor.services.RequestService +import java.io.IOException +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.htmlunit.WebClient import org.htmlunit.html.HtmlPage @@ -51,261 +54,248 @@ import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder -import java.io.IOException -import java.time.Instant -import java.util.* @WebMvcTest(controllers = [HomeController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( - classes = [ - HomeController::class, - AppConfiguration::class, - AppSecurityConfiguration::class - ] + classes = [HomeController::class, AppConfiguration::class, AppSecurityConfiguration::class] ) @TestPropertySource( - properties = [ - "app.pseudonymize.generator=BUILDIN", - "app.security.admin-user=admin", - "app.security.admin-password={noop}very-secret" - ] -) -@MockitoBean( - types = [RequestService::class] + properties = + [ + "app.pseudonymize.generator=BUILDIN", + "app.security.admin-user=admin", + "app.security.admin-password={noop}very-secret", + ] ) +@MockitoBean(types = [RequestService::class]) class HomeControllerTest { - private lateinit var mockMvc: MockMvc - private lateinit var webClient: WebClient + private lateinit var mockMvc: MockMvc + private lateinit var webClient: WebClient - @BeforeEach - fun setup( - @Autowired mockMvc: MockMvc, - @Autowired requestService: RequestService - ) { - this.mockMvc = mockMvc - this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build() + @BeforeEach + fun setup(@Autowired mockMvc: MockMvc, @Autowired requestService: RequestService) { + this.mockMvc = mockMvc + this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build() - whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty()) - } + whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty()) + } - @Test - fun testShouldRequestHomePage() { - mockMvc.get("/").andExpect { - status { isOk() } - view { name("index") } - } + @Test + fun testShouldRequestHomePage() { + mockMvc.get("/").andExpect { + status { isOk() } + view { name("index") } } + } - @Nested - inner class WithRequests { - - private lateinit var requestService: RequestService - - @BeforeEach - fun setup( - @Autowired requestService: RequestService - ) { - this.requestService = requestService - } - - @Test - fun testShouldShowHomePage() { - whenever(requestService.findAll(any<Pageable>())).thenReturn( - PageImpl( - listOf( - Request( - 2L, - randomRequestId(), - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("ashdkasdh"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS - ), - Request( - 1L, - randomRequestId(), - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("asdasdasd"), - RequestType.MTB_FILE, - RequestStatus.ERROR - ) - ) - ) - ) - - val page = webClient.getPage<HtmlPage>("http://localhost/") - assertThat(page.querySelectorAll("tbody tr")).hasSize(2) - assertThat(page.querySelectorAll("div.notification.info")).isEmpty() - } - - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldShowRequestDetails() { - val requestId = randomRequestId() - - whenever(requestService.findByUuid(anyValueClass())).thenReturn( - Optional.of( - Request( - 2L, - requestId, - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("ashdkasdh"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS, - Instant.now(), - Report("Test") - ) - ) - ) - - val page = webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}") - assertThat(page.querySelectorAll("tbody tr")).hasSize(1) - assertThat(page.querySelectorAll("div.notification.info")).isEmpty() - } - - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldShowPatientDetails() { - whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn( - PageImpl( - listOf( - Request( - 2L, - randomRequestId(), - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("ashdkasdh"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS - ), - Request( - 1L, - randomRequestId(), - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("asdasdasd"), - RequestType.MTB_FILE, - RequestStatus.ERROR - ) - ) - ) - ) - - val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") - assertThat(page.querySelectorAll("tbody tr")).hasSize(2) - assertThat(page.querySelectorAll("div.notification.info")).isEmpty() - } + @Nested + inner class WithRequests { - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldShowPatientPseudonym() { - whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn( - PageImpl( - listOf( - Request( - 2L, - randomRequestId(), - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("ashdkasdh"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS - ), - Request( - 1L, - randomRequestId(), - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("asdasdasd"), - RequestType.MTB_FILE, - RequestStatus.ERROR - ) - ) - ) - ) + private lateinit var requestService: RequestService - val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") - assertThat(page.querySelectorAll("h2 > span")).hasSize(1) - assertThat(page.querySelectorAll("h2 > span").first().textContent).isEqualTo("PSEUDO1") - } + @BeforeEach + fun setup(@Autowired requestService: RequestService) { + this.requestService = requestService + } + @Test + fun testShouldShowHomePage() { + whenever(requestService.findAll(any<Pageable>())) + .thenReturn( + PageImpl( + listOf( + Request( + 2L, + randomRequestId(), + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("ashdkasdh"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + ), + Request( + 1L, + randomRequestId(), + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("asdasdasd"), + RequestType.MTB_FILE, + RequestStatus.ERROR, + ), + ) + ) + ) + + val page = webClient.getPage<HtmlPage>("http://localhost/") + assertThat(page.querySelectorAll("tbody tr")).hasSize(2) + assertThat(page.querySelectorAll("div.notification.info")).isEmpty() } - @Nested - inner class WithoutRequests { + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldShowRequestDetails() { + val requestId = randomRequestId() + + whenever(requestService.findByUuid(anyValueClass())) + .thenReturn( + Optional.of( + Request( + 2L, + requestId, + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("ashdkasdh"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + Instant.now(), + Report("Test"), + ) + ) + ) + + val page = webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}") + assertThat(page.querySelectorAll("tbody tr")).hasSize(1) + assertThat(page.querySelectorAll("div.notification.info")).isEmpty() + } - private lateinit var requestService: RequestService + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldShowPatientDetails() { + whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())) + .thenReturn( + PageImpl( + listOf( + Request( + 2L, + randomRequestId(), + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("ashdkasdh"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + ), + Request( + 1L, + randomRequestId(), + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("asdasdasd"), + RequestType.MTB_FILE, + RequestStatus.ERROR, + ), + ) + ) + ) + + val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") + assertThat(page.querySelectorAll("tbody tr")).hasSize(2) + assertThat(page.querySelectorAll("div.notification.info")).isEmpty() + } - @BeforeEach - fun setup( - @Autowired requestService: RequestService - ) { - this.requestService = requestService + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldShowPatientPseudonym() { + whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())) + .thenReturn( + PageImpl( + listOf( + Request( + 2L, + randomRequestId(), + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("ashdkasdh"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + ), + Request( + 1L, + randomRequestId(), + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("asdasdasd"), + RequestType.MTB_FILE, + RequestStatus.ERROR, + ), + ) + ) + ) + + val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") + assertThat(page.querySelectorAll("h2 > span")).hasSize(1) + assertThat(page.querySelectorAll("h2 > span").first().textContent).isEqualTo("PSEUDO1") + } + } - whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty()) - } + @Nested + inner class WithoutRequests { - @Test - fun testShouldShowHomePage() { - val page = webClient.getPage<HtmlPage>("http://localhost/") - assertThat(page.querySelectorAll("tbody tr")).isEmpty() - assertThat(page.querySelectorAll("div.notification.info")).hasSize(1) - } + private lateinit var requestService: RequestService - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldThrowNotFoundExceptionForUnknownReport() { - val requestId = randomRequestId() + @BeforeEach + fun setup(@Autowired requestService: RequestService) { + this.requestService = requestService - whenever(requestService.findByUuid(anyValueClass())).thenReturn( - Optional.empty() - ) + whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty()) + } - assertThrows<IOException> { - webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}") - }.also { - assertThat(it).hasRootCauseInstanceOf(NotFoundException::class.java) - } - } + @Test + fun testShouldShowHomePage() { + val page = webClient.getPage<HtmlPage>("http://localhost/") + assertThat(page.querySelectorAll("tbody tr")).isEmpty() + assertThat(page.querySelectorAll("div.notification.info")).hasSize(1) + } - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldShowEmptyPatientDetails() { - whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn(Page.empty()) + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldThrowNotFoundExceptionForUnknownReport() { + val requestId = randomRequestId() - val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") - assertThat(page.querySelectorAll("tbody tr")).isEmpty() - assertThat(page.querySelectorAll("div.notification.info")).hasSize(1) - } + whenever(requestService.findByUuid(anyValueClass())).thenReturn(Optional.empty()) - @Test - @WithMockUser(username = "admin", roles = ["ADMIN"]) - fun testShouldShowNoConsentStatusBadge() { - whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())).thenReturn( - PageImpl( - listOf( - Request( - 1L, - randomRequestId(), - PatientPseudonym("PSEUDO1"), - PatientId("PATIENT1"), - Fingerprint("ashdkasdh"), - RequestType.MTB_FILE, - RequestStatus.NO_CONSENT - ) - ) - ) - ) + assertThrows<IOException> { + webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}") + } + .also { assertThat(it).hasRootCauseInstanceOf(NotFoundException::class.java) } + } - val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") - assertThat(page.querySelectorAll("tbody tr")).hasSize(1) - assertThat(page.querySelectorAll("tbody tr > td > small").first().textContent).isEqualTo("NO_CONSENT") - } + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldShowEmptyPatientDetails() { + whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())) + .thenReturn(Page.empty()) + + val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") + assertThat(page.querySelectorAll("tbody tr")).isEmpty() + assertThat(page.querySelectorAll("div.notification.info")).hasSize(1) } + @Test + @WithMockUser(username = "admin", roles = ["ADMIN"]) + fun testShouldShowNoConsentStatusBadge() { + whenever(requestService.findRequestByPatientId(anyValueClass(), any<Pageable>())) + .thenReturn( + PageImpl( + listOf( + Request( + 1L, + randomRequestId(), + PatientPseudonym("PSEUDO1"), + PatientId("PATIENT1"), + Fingerprint("ashdkasdh"), + RequestType.MTB_FILE, + RequestStatus.NO_CONSENT, + ) + ) + ) + ) + + val page = webClient.getPage<HtmlPage>("http://localhost/patient/PSEUDO1") + assertThat(page.querySelectorAll("tbody tr")).hasSize(1) + assertThat(page.querySelectorAll("tbody tr > td > small").first().textContent) + .isEqualTo("NO_CONSENT") + } + } } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt index 54ad6e8..bd8014a 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt @@ -42,30 +42,26 @@ import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder @WebMvcTest(controllers = [LoginController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( - classes = [ - LoginController::class, - AppConfiguration::class, - AppSecurityConfiguration::class - ] + classes = [LoginController::class, AppConfiguration::class, AppSecurityConfiguration::class], ) @TestPropertySource( - properties = [ - "app.pseudonymize.generator=BUILDIN", - "app.security.admin-user=admin", - "app.security.admin-password={noop}very-secret", - "app.security.enable-tokens=true" - ] -) -@MockitoBean( - types = [TokenService::class] + properties = + [ + "app.pseudonymize.generator=BUILDIN", + "app.security.admin-user=admin", + "app.security.admin-password={noop}very-secret", + "app.security.enable-tokens=true", + ], ) +@MockitoBean(types = [TokenService::class]) class LoginControllerTest { - private lateinit var mockMvc: MockMvc private lateinit var webClient: WebClient @BeforeEach - fun setup(@Autowired mockMvc: MockMvc) { + fun setup( + @Autowired mockMvc: MockMvc, + ) { this.mockMvc = mockMvc this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build() } @@ -82,7 +78,11 @@ class LoginControllerTest { fun testShouldShowLoginForm() { val page = webClient.getPage<HtmlPage>("http://localhost/login") assertThat( - page.getElementsByTagName("main").first().firstElementChild.getAttribute("class") + page + .getElementsByTagName("main") + .first() + .firstElementChild + .getAttribute("class"), ).isEqualTo("login-form") } } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt index b55a702..f91e3cf 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt @@ -38,26 +38,25 @@ import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder @WebMvcTest(controllers = [StatisticsController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( - classes = [ - StatisticsController::class, - AppConfiguration::class, - AppSecurityConfiguration::class - ] + classes = + [StatisticsController::class, AppConfiguration::class, AppSecurityConfiguration::class], ) @TestPropertySource( - properties = [ - "app.pseudonymize.generator=BUILDIN", - "app.security.admin-user=admin", - "app.security.admin-password={noop}very-secret" - ] + properties = + [ + "app.pseudonymize.generator=BUILDIN", + "app.security.admin-user=admin", + "app.security.admin-password={noop}very-secret", + ], ) class StatisticsControllerTest { - private lateinit var mockMvc: MockMvc private lateinit var webClient: WebClient @BeforeEach - fun setup(@Autowired mockMvc: MockMvc) { + fun setup( + @Autowired mockMvc: MockMvc, + ) { this.mockMvc = mockMvc this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build() } @@ -69,5 +68,4 @@ class StatisticsControllerTest { view { name("statistics") } } } - } diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt index f0c3b63..16a9464 100644 --- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt +++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt @@ -57,28 +57,22 @@ import java.time.Instant import java.time.ZoneId import java.time.temporal.ChronoUnit - @WebMvcTest(controllers = [StatisticsRestController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( - classes = [ - StatisticsRestController::class, - AppConfiguration::class, - AppSecurityConfiguration::class - ] + classes = + [StatisticsRestController::class, AppConfiguration::class, AppSecurityConfiguration::class], ) @TestPropertySource( - properties = [ - "app.pseudonymize.generator=BUILDIN", - "app.security.admin-user=admin", - "app.security.admin-password={noop}very-secret" - ] -) -@MockitoBean( - types = [RequestService::class] + properties = + [ + "app.pseudonymize.generator=BUILDIN", + "app.security.admin-user=admin", + "app.security.admin-password={noop}very-secret", + ], ) +@MockitoBean(types = [RequestService::class]) class StatisticsRestControllerTest { - private lateinit var mockMvc: MockMvc private lateinit var statisticsUpdateProducer: Sinks.Many<Any> @@ -88,7 +82,7 @@ class StatisticsRestControllerTest { fun setup( @Autowired mockMvc: MockMvc, @Autowired statisticsUpdateProducer: Sinks.Many<Any>, - @Autowired requestService: RequestService + @Autowired requestService: RequestService, ) { this.mockMvc = mockMvc this.statisticsUpdateProducer = statisticsUpdateProducer @@ -100,40 +94,38 @@ class StatisticsRestControllerTest { @Test fun testShouldRequestStatesForMtbFiles() { doAnswer { _ -> - listOf( - CountedState(42, RequestStatus.WARNING), - CountedState(1, RequestStatus.UNKNOWN) - ) - }.whenever(requestService).countStates() + listOf(CountedState(42, RequestStatus.WARNING), CountedState(1, RequestStatus.UNKNOWN)) + }.whenever(requestService) + .countStates() mockMvc.get("/statistics/requeststates").andExpect { - status { isOk() }.also { - jsonPath("$", hasSize<Int>(2)) - jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name)) - jsonPath("$[0].value", equalTo(42)) - jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name)) - jsonPath("$[1].value", equalTo(1)) - } + status { isOk() } + .also { + jsonPath("$", hasSize<Int>(2)) + jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name)) + jsonPath("$[0].value", equalTo(42)) + jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name)) + jsonPath("$[1].value", equalTo(1)) + } } } @Test fun testShouldRequestStatesForDeletes() { doAnswer { _ -> - listOf( - CountedState(42, RequestStatus.SUCCESS), - CountedState(1, RequestStatus.ERROR) - ) - }.whenever(requestService).countDeleteStates() + listOf(CountedState(42, RequestStatus.SUCCESS), CountedState(1, RequestStatus.ERROR)) + }.whenever(requestService) + .countDeleteStates() mockMvc.get("/statistics/requeststates?delete=true").andExpect { - status { isOk() }.also { - jsonPath("$", hasSize<Int>(2)) - jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name)) - jsonPath("$[0].value", equalTo(42)) - jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name)) - jsonPath("$[1].value", equalTo(1)) - } + status { isOk() } + .also { + jsonPath("$", hasSize<Int>(2)) + jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name)) + jsonPath("$[0].value", equalTo(42)) + jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name)) + jsonPath("$[1].value", equalTo(1)) + } } } } @@ -143,47 +135,44 @@ class StatisticsRestControllerTest { @Test fun testShouldRequestPatientStatesForMtbFiles() { doAnswer { _ -> - listOf( - CountedState(42, RequestStatus.WARNING), - CountedState(1, RequestStatus.UNKNOWN) - ) - }.whenever(requestService).findPatientUniqueStates() + listOf(CountedState(42, RequestStatus.WARNING), CountedState(1, RequestStatus.UNKNOWN)) + }.whenever(requestService) + .findPatientUniqueStates() mockMvc.get("/statistics/requestpatientstates").andExpect { - status { isOk() }.also { - jsonPath("$", hasSize<Int>(2)) - jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name)) - jsonPath("$[0].value", equalTo(42)) - jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name)) - jsonPath("$[1].value", equalTo(1)) - } + status { isOk() } + .also { + jsonPath("$", hasSize<Int>(2)) + jsonPath("$[0].name", equalTo(RequestStatus.WARNING.name)) + jsonPath("$[0].value", equalTo(42)) + jsonPath("$[1].name", equalTo(RequestStatus.UNKNOWN.name)) + jsonPath("$[1].value", equalTo(1)) + } } } @Test fun testShouldRequestPatientStatesForDeletes() { doAnswer { _ -> - listOf( - CountedState(42, RequestStatus.SUCCESS), - CountedState(1, RequestStatus.ERROR) - ) - }.whenever(requestService).findPatientUniqueDeleteStates() + listOf(CountedState(42, RequestStatus.SUCCESS), CountedState(1, RequestStatus.ERROR)) + }.whenever(requestService) + .findPatientUniqueDeleteStates() mockMvc.get("/statistics/requestpatientstates?delete=true").andExpect { - status { isOk() }.also { - jsonPath("$", hasSize<Int>(2)) - jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name)) - jsonPath("$[0].value", equalTo(42)) - jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name)) - jsonPath("$[1].value", equalTo(1)) - } + status { isOk() } + .also { + jsonPath("$", hasSize<Int>(2)) + jsonPath("$[0].name", equalTo(RequestStatus.SUCCESS.name)) + jsonPath("$[0].value", equalTo(42)) + jsonPath("$[1].name", equalTo(RequestStatus.ERROR.name)) + jsonPath("$[1].value", equalTo(1)) + } } } } @Nested inner class LastMonthStatesTest { - @BeforeEach fun setup() { val zoneId = ZoneId.of("Europe/Berlin") @@ -197,7 +186,12 @@ class StatisticsRestControllerTest { Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, RequestStatus.SUCCESS, - Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS).toInstant() + Instant + .now() + .atZone(zoneId) + .truncatedTo(ChronoUnit.DAYS) + .minus(2, ChronoUnit.DAYS) + .toInstant(), ), Request( 2, @@ -207,7 +201,12 @@ class StatisticsRestControllerTest { Fingerprint("0123456789abcdef2"), RequestType.MTB_FILE, RequestStatus.WARNING, - Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS).toInstant() + Instant + .now() + .atZone(zoneId) + .truncatedTo(ChronoUnit.DAYS) + .minus(2, ChronoUnit.DAYS) + .toInstant(), ), Request( 3, @@ -217,7 +216,12 @@ class StatisticsRestControllerTest { Fingerprint("0123456789abcdee1"), RequestType.DELETE, RequestStatus.ERROR, - Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS).toInstant() + Instant + .now() + .atZone(zoneId) + .truncatedTo(ChronoUnit.DAYS) + .minus(1, ChronoUnit.DAYS) + .toInstant(), ), Request( 4, @@ -227,7 +231,12 @@ class StatisticsRestControllerTest { Fingerprint("0123456789abcdef2"), RequestType.MTB_FILE, RequestStatus.DUPLICATION, - Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS).toInstant() + Instant + .now() + .atZone(zoneId) + .truncatedTo(ChronoUnit.DAYS) + .minus(1, ChronoUnit.DAYS) + .toInstant(), ), Request( 5, @@ -237,49 +246,54 @@ class StatisticsRestControllerTest { Fingerprint("0123456789abcdef2"), RequestType.DELETE, RequestStatus.UNKNOWN, - Instant.now().atZone(zoneId).truncatedTo(ChronoUnit.DAYS).toInstant() + Instant + .now() + .atZone(zoneId) + .truncatedTo(ChronoUnit.DAYS) + .toInstant(), ), ) - }.whenever(requestService).findAll() + }.whenever(requestService) + .findAll() } @Test fun testShouldRequestLastMonthForMtbFiles() { mockMvc.get("/statistics/requestslastmonth").andExpect { - status { isOk() }.also { - jsonPath("$", hasSize<Int>(31)) - }.also { - jsonPath("$[28].nameValues.error", equalTo(0)) - jsonPath("$[28].nameValues.warning", equalTo(1)) - jsonPath("$[28].nameValues.success", equalTo(1)) - jsonPath("$[28].nameValues.duplication", equalTo(0)) - jsonPath("$[28].nameValues.unknown", equalTo(0)) - jsonPath("$[29].nameValues.error", equalTo(0)) - jsonPath("$[29].nameValues.warning", equalTo(0)) - jsonPath("$[29].nameValues.success", equalTo(0)) - jsonPath("$[29].nameValues.duplication", equalTo(1)) - jsonPath("$[29].nameValues.unknown", equalTo(0)) - } + status { isOk() } + .also { jsonPath("$", hasSize<Int>(31)) } + .also { + jsonPath("$[28].nameValues.error", equalTo(0)) + jsonPath("$[28].nameValues.warning", equalTo(1)) + jsonPath("$[28].nameValues.success", equalTo(1)) + jsonPath("$[28].nameValues.duplication", equalTo(0)) + jsonPath("$[28].nameValues.unknown", equalTo(0)) + jsonPath("$[29].nameValues.error", equalTo(0)) + jsonPath("$[29].nameValues.warning", equalTo(0)) + jsonPath("$[29].nameValues.success", equalTo(0)) + jsonPath("$[29].nameValues.duplication", equalTo(1)) + jsonPath("$[29].nameValues.unknown", equalTo(0)) + } } } @Test fun testShouldRequestLastMonthForDeletes() { mockMvc.get("/statistics/requestslastmonth?delete=true").andExpect { - status { isOk() }.also { - jsonPath("$", hasSize<Int>(31)) - }.also { - jsonPath("$[29].nameValues.error", equalTo(1)) - jsonPath("$[29].nameValues.warning", equalTo(0)) - jsonPath("$[29].nameValues.success", equalTo(0)) - jsonPath("$[29].nameValues.duplication", equalTo(0)) - jsonPath("$[29].nameValues.unknown", equalTo(0)) - jsonPath("$[30].nameValues.error", equalTo(0)) - jsonPath("$[30].nameValues.warning", equalTo(0)) - jsonPath("$[30].nameValues.success", equalTo(0)) - jsonPath("$[30].nameValues.duplication", equalTo(0)) - jsonPath("$[30].nameValues.unknown", equalTo(1)) - } + status { isOk() } + .also { jsonPath("$", hasSize<Int>(31)) } + .also { + jsonPath("$[29].nameValues.error", equalTo(1)) + jsonPath("$[29].nameValues.warning", equalTo(0)) + jsonPath("$[29].nameValues.success", equalTo(0)) + jsonPath("$[29].nameValues.duplication", equalTo(0)) + jsonPath("$[29].nameValues.unknown", equalTo(0)) + jsonPath("$[30].nameValues.error", equalTo(0)) + jsonPath("$[30].nameValues.warning", equalTo(0)) + jsonPath("$[30].nameValues.success", equalTo(0)) + jsonPath("$[30].nameValues.duplication", equalTo(0)) + jsonPath("$[30].nameValues.unknown", equalTo(1)) + } } } } @@ -289,26 +303,27 @@ class StatisticsRestControllerTest { private lateinit var webClient: WebTestClient @BeforeEach - fun setup( - applicationContext: WebApplicationContext, - ) { - this.webClient = MockMvcWebTestClient - .bindToApplicationContext(applicationContext).build() + fun setup(applicationContext: WebApplicationContext) { + this.webClient = MockMvcWebTestClient.bindToApplicationContext(applicationContext).build() } @Test fun testShouldRequestSSE() { statisticsUpdateProducer.emitComplete { _, _ -> true } - val result = webClient.get().uri("http://localhost/statistics/events").accept(TEXT_EVENT_STREAM).exchange() - .expectStatus().isOk() - .expectHeader().contentType(TEXT_EVENT_STREAM) - .returnResult(String::class.java) + val result = + webClient + .get() + .uri("http://localhost/statistics/events") + .accept(TEXT_EVENT_STREAM) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType(TEXT_EVENT_STREAM) + .returnResult(String::class.java) - StepVerifier.create(result.responseBody) - .expectComplete() - .verify() + StepVerifier.create(result.responseBody).expectComplete().verify() } } - } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt b/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt index 32d0954..1c590fc 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/Exceptions.kt @@ -19,4 +19,4 @@ package dev.dnpm.etl.processor -class NotFoundException : RuntimeException()
\ No newline at end of file +class NotFoundException : RuntimeException() diff --git a/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt index 2618b09..e35cddf 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/ServletInitializer.kt @@ -23,9 +23,6 @@ import org.springframework.boot.builder.SpringApplicationBuilder import org.springframework.boot.web.servlet.support.SpringBootServletInitializer class ServletInitializer : SpringBootServletInitializer() { - - override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder { - return application.sources(EtlProcessorApplication::class.java) - } - + override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder = + application.sources(EtlProcessorApplication::class.java) } 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 57ab06c..8786c34 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfigProperties.kt @@ -28,7 +28,7 @@ data class AppConfigProperties( var transformations: List<TransformationProperties> = listOf(), var maxRetryAttempts: Int = 3, var duplicationDetection: Boolean = true, - var genomDeTestSubmission: Boolean = false + var genomDeTestSubmission: Boolean = false, ) { companion object { const val NAME = "app" @@ -49,8 +49,7 @@ data class PseudonymizeConfigProperties( data class GPasConfigProperties( val uri: String?, val soapEndpoint: String?, - @get:DeprecatedConfigurationProperty(since = "0.12") - val pidDomain: String?, + @get:DeprecatedConfigurationProperty(since = "0.12") val pidDomain: String?, val patientDomain: String = pidDomain ?: "etl-processor", val genomDeTanDomain: String = "ccdn", val username: String?, @@ -63,7 +62,7 @@ data class GPasConfigProperties( @ConfigurationProperties(ConsentConfigProperties.NAME) data class ConsentConfigProperties( - var service: ConsentService = ConsentService.NONE + var service: ConsentService = ConsentService.NONE, ) { companion object { const val NAME = "app.consent" @@ -72,60 +71,33 @@ data class ConsentConfigProperties( @ConfigurationProperties(GIcsConfigProperties.NAME) data class GIcsConfigProperties( - /** - * Base URL to gICS System - * - */ + /** Base URL to gICS System */ val uri: String?, val username: String? = null, val password: String? = null, - /** * gICS specific system - * **/ + * * + */ val personIdentifierSystem: String = "https://ths-greifswald.de/fhir/gics/identifiers/Patienten-ID", - - /** - * Domain of broad consent resources - **/ + /** Domain of broad consent resources */ val broadConsentDomainName: String = "MII", - - /** - * Domain of Modelvorhaben 64e consent resources - **/ + /** Domain of Modelvorhaben 64e consent resources */ val genomDeConsentDomainName: String = "GenomDE_MV", - - /** - * Value to expect in case of positiv consent - */ + /** Value to expect in case of positiv consent */ val broadConsentPolicyCode: String = "2.16.840.1.113883.3.1937.777.24.5.3.6", - - /** - * Consent Policy which should be used for consent check - */ + /** Consent Policy which should be used for consent check */ val broadConsentPolicySystem: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", - - /** - * Consent Policy uri for MII Broad Consent Version - */ + /** Consent Policy uri for MII Broad Consent Version */ val broadConsentPolicyUri: String = "urn:oid:2.16.840.1.113883.3.1937.777.24.2.1790", - - /** - * Value to expect in case of positiv consent - */ + /** Value to expect in case of positiv consent */ val genomeDePolicyCode: String = "sequencing", - - /** - * Consent Policy which should be used for consent check - */ - val genomeDePolicySystem: String = "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", - - /** - * Consent version (fixed version) - * - */ - val genomeDeConsentVersion: String = "2.0" + /** Consent Policy which should be used for consent check */ + val genomeDePolicySystem: String = + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + /** Consent version (fixed version) */ + val genomeDeConsentVersion: String = "2.0", ) { companion object { const val NAME = "app.consent.gics" @@ -136,7 +108,7 @@ data class GIcsConfigProperties( data class RestTargetProperties( val uri: String?, val username: String?, - val password: String? + val password: String?, ) { companion object { const val NAME = "app.rest" @@ -149,7 +121,7 @@ data class KafkaProperties( val outputTopic: String = "etl-processor", val outputResponseTopic: String = "${outputTopic}_response", val groupId: String = "${outputTopic}_group", - val servers: String = "" + val servers: String = "", ) { companion object { const val NAME = "app.kafka" @@ -162,7 +134,7 @@ data class SecurityConfigProperties( val adminPassword: String?, val enableTokens: Boolean = false, val enableOidc: Boolean = false, - val defaultNewUserRole: Role = Role.USER + val defaultNewUserRole: Role = Role.USER, ) { companion object { const val NAME = "app.security" @@ -171,16 +143,16 @@ data class SecurityConfigProperties( enum class PseudonymGenerator { BUILDIN, - GPAS + GPAS, } enum class ConsentService { NONE, - GICS + GICS, } data class TransformationProperties( val path: String, val from: String, - val to: String + val to: 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 de302fd..36b0a75 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppConfiguration.kt @@ -30,6 +30,8 @@ import dev.dnpm.etl.processor.security.TokenService import dev.dnpm.etl.processor.services.ConsentProcessor import dev.dnpm.etl.processor.services.Transformation import dev.dnpm.etl.processor.services.TransformationService +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration import org.apache.cxf.jaxws.JaxWsProxyFactoryBean import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.AnyNestedCondition @@ -53,258 +55,251 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.web.client.HttpClientErrorException import org.springframework.web.client.RestTemplate import reactor.core.publisher.Sinks -import kotlin.time.Duration.Companion.seconds -import kotlin.time.toJavaDuration - @Configuration @EnableConfigurationProperties( - value = [ - AppConfigProperties::class, - PseudonymizeConfigProperties::class, - GPasConfigProperties::class, - ConsentConfigProperties::class, - GIcsConfigProperties::class - ] + value = + [ + AppConfigProperties::class, + PseudonymizeConfigProperties::class, + GPasConfigProperties::class, + ConsentConfigProperties::class, + GIcsConfigProperties::class, + ] ) @EnableScheduling class AppConfiguration { - private val logger = LoggerFactory.getLogger(AppConfiguration::class.java) + private val logger = LoggerFactory.getLogger(AppConfiguration::class.java) - @Bean - fun restTemplate(): RestTemplate { - return RestTemplate() - } + @Bean + fun restTemplate(): RestTemplate { + return RestTemplate() + } - @Bean - fun appFhirConfig(): AppFhirConfig { - return AppFhirConfig() - } + @Bean + fun appFhirConfig(): AppFhirConfig { + return AppFhirConfig() + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) - @Bean - fun gpasSoapProxyFactoryBean(gpasConfigProperties: GPasConfigProperties): JaxWsProxyFactoryBean { - val proxyFactory = JaxWsProxyFactoryBean() - proxyFactory.serviceClass = GpasSoapService::class.java - proxyFactory.address = gpasConfigProperties.soapEndpoint - return proxyFactory - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) + @Bean + fun gpasSoapProxyFactoryBean(gpasConfigProperties: GPasConfigProperties): JaxWsProxyFactoryBean { + val proxyFactory = JaxWsProxyFactoryBean() + proxyFactory.serviceClass = GpasSoapService::class.java + proxyFactory.address = gpasConfigProperties.soapEndpoint + return proxyFactory + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) - @Bean - fun gpasSoapProxy(gpasConfigProperties: GPasConfigProperties): GpasSoapService { - return gpasSoapProxyFactoryBean(gpasConfigProperties).create() as GpasSoapService - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) + @Bean + fun gpasSoapProxy(gpasConfigProperties: GPasConfigProperties): GpasSoapService { + return gpasSoapProxyFactoryBean(gpasConfigProperties).create() as GpasSoapService + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) - @Bean - fun gpasSoapPseudonymGenerator( - configProperties: GPasConfigProperties, - retryTemplate: RetryTemplate, - gpasSoapService: GpasSoapService, - appFhirConfig: AppFhirConfig - ): Generator { - logger.info("Selected 'GpasSoapPseudonym Generator'") - return GpasSoapPseudonymGenerator(configProperties, retryTemplate, gpasSoapService, appFhirConfig) - } - - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @ConditionalOnProperty(value = ["app.pseudonymize.gpas.uri"]) - @Bean - fun gpasPseudonymGenerator( - configProperties: GPasConfigProperties, - retryTemplate: RetryTemplate, - restTemplate: RestTemplate, - appFhirConfig: AppFhirConfig - ): Generator { - logger.info("Selected 'GpasPseudonym Generator'") - return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig) - } - - @ConditionalOnProperty( - value = ["app.pseudonymize.generator"], - havingValue = "BUILDIN", - matchIfMissing = true + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.soap-endpoint"]) + @Bean + fun gpasSoapPseudonymGenerator( + configProperties: GPasConfigProperties, + retryTemplate: RetryTemplate, + gpasSoapService: GpasSoapService, + appFhirConfig: AppFhirConfig, + ): Generator { + logger.info("Selected 'GpasSoapPseudonym Generator'") + return GpasSoapPseudonymGenerator( + configProperties, + retryTemplate, + gpasSoapService, + appFhirConfig, ) - @Bean - fun buildinPseudonymGenerator(): Generator { - logger.info("Selected 'BUILDIN Pseudonym Generator'") - return AnonymizingGenerator() - } + } - @Bean - fun pseudonymizeService( - generator: Generator, - pseudonymizeConfigProperties: PseudonymizeConfigProperties - ): PseudonymizeService { - return PseudonymizeService(generator, pseudonymizeConfigProperties) - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @ConditionalOnProperty(value = ["app.pseudonymize.gpas.uri"]) + @Bean + fun gpasPseudonymGenerator( + configProperties: GPasConfigProperties, + retryTemplate: RetryTemplate, + restTemplate: RestTemplate, + appFhirConfig: AppFhirConfig, + ): Generator { + logger.info("Selected 'GpasPseudonym Generator'") + return GpasPseudonymGenerator(configProperties, retryTemplate, restTemplate, appFhirConfig) + } - @Bean - fun reportService(): ReportService { - return ReportService(getObjectMapper()) - } + @ConditionalOnProperty( + value = ["app.pseudonymize.generator"], + havingValue = "BUILDIN", + matchIfMissing = true, + ) + @Bean + fun buildinPseudonymGenerator(): Generator { + logger.info("Selected 'BUILDIN Pseudonym Generator'") + return AnonymizingGenerator() + } - @Bean - fun getObjectMapper(): ObjectMapper { - return JacksonConfig().objectMapper() - } + @Bean + fun pseudonymizeService( + generator: Generator, + pseudonymizeConfigProperties: PseudonymizeConfigProperties, + ): PseudonymizeService { + return PseudonymizeService(generator, pseudonymizeConfigProperties) + } - @Bean - fun transformationService( - configProperties: AppConfigProperties - ): TransformationService { - logger.info("Apply ${configProperties.transformations.size} transformation rules") - return TransformationService(getObjectMapper(), configProperties.transformations.map { - Transformation.of(it.path) from it.from to it.to - }) - } + @Bean + fun reportService(): ReportService { + return ReportService(getObjectMapper()) + } - @Bean - fun retryTemplate(configProperties: AppConfigProperties): RetryTemplate { - return RetryTemplateBuilder() - .notRetryOn(IllegalArgumentException::class.java) - .notRetryOn(HttpClientErrorException.BadRequest::class.java) - .notRetryOn(HttpClientErrorException.UnprocessableEntity::class.java) - .exponentialBackoff(2.seconds.toJavaDuration(), 1.25, 5.seconds.toJavaDuration()) - .customPolicy(SimpleRetryPolicy(configProperties.maxRetryAttempts)) - .withListener(object : RetryListener { - override fun <T : Any, E : Throwable> onError( - context: RetryContext, - callback: RetryCallback<T, E>, - throwable: Throwable - ) { - logger.warn( - "Error occured: {}. Retrying {}", - throwable.message, - context.retryCount - ) - } - }) - .build() - } + @Bean + fun getObjectMapper(): ObjectMapper { + return JacksonConfig().objectMapper() + } - @ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true") - @Bean - fun tokenService( - userDetailsManager: InMemoryUserDetailsManager, - passwordEncoder: PasswordEncoder, - tokenRepository: TokenRepository - ): TokenService { - return TokenService(userDetailsManager, passwordEncoder, tokenRepository) - } + @Bean + fun transformationService(configProperties: AppConfigProperties): TransformationService { + logger.info("Apply ${configProperties.transformations.size} transformation rules") + return TransformationService( + getObjectMapper(), + configProperties.transformations.map { Transformation.of(it.path) from it.from to it.to }, + ) + } - @Bean - fun statisticsUpdateProducer(): Sinks.Many<Any> { - return Sinks.many().multicast().directBestEffort() - } + @Bean + fun retryTemplate(configProperties: AppConfigProperties): RetryTemplate { + return RetryTemplateBuilder() + .notRetryOn(IllegalArgumentException::class.java) + .notRetryOn(HttpClientErrorException.BadRequest::class.java) + .notRetryOn(HttpClientErrorException.UnprocessableEntity::class.java) + .exponentialBackoff(2.seconds.toJavaDuration(), 1.25, 5.seconds.toJavaDuration()) + .customPolicy(SimpleRetryPolicy(configProperties.maxRetryAttempts)) + .withListener( + object : RetryListener { + override fun <T : Any, E : Throwable> onError( + context: RetryContext, + callback: RetryCallback<T, E>, + throwable: Throwable, + ) { + logger.warn("Error occured: {}. Retrying {}", throwable.message, context.retryCount) + } + } + ) + .build() + } - @Bean - fun connectionCheckUpdateProducer(): Sinks.Many<ConnectionCheckResult> { - return Sinks.many().multicast().onBackpressureBuffer() - } + @ConditionalOnProperty(value = ["app.security.enable-tokens"], havingValue = "true") + @Bean + fun tokenService( + userDetailsManager: InMemoryUserDetailsManager, + passwordEncoder: PasswordEncoder, + tokenRepository: TokenRepository, + ): TokenService { + return TokenService(userDetailsManager, passwordEncoder, tokenRepository) + } - @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") - @Bean - fun gPasConnectionCheckService( - restTemplate: RestTemplate, - gPasConfigProperties: GPasConfigProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return GPasConnectionCheckService( - restTemplate, - gPasConfigProperties, - connectionCheckUpdateProducer - ) - } + @Bean + fun statisticsUpdateProducer(): Sinks.Many<Any> { + return Sinks.many().multicast().directBestEffort() + } - @ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS") - @ConditionalOnMissingBean - @Bean - fun gPasConnectionCheckServiceOnDeprecatedProperty( - restTemplate: RestTemplate, - gPasConfigProperties: GPasConfigProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return GPasConnectionCheckService( - restTemplate, - gPasConfigProperties, - connectionCheckUpdateProducer - ) - } + @Bean + fun connectionCheckUpdateProducer(): Sinks.Many<ConnectionCheckResult> { + return Sinks.many().multicast().onBackpressureBuffer() + } - @Bean - fun jdbcConfiguration(): AbstractJdbcConfiguration { - return AppJdbcConfiguration() - } + @ConditionalOnProperty(value = ["app.pseudonymize.generator"], havingValue = "GPAS") + @Bean + fun gPasConnectionCheckService( + restTemplate: RestTemplate, + gPasConfigProperties: GPasConfigProperties, + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService { + return GPasConnectionCheckService( + restTemplate, + gPasConfigProperties, + connectionCheckUpdateProducer, + ) + } - @Conditional(GicsEnabledCondition::class) - @Bean - fun gicsConsentService( - gIcsConfigProperties: GIcsConfigProperties, - retryTemplate: RetryTemplate, - restTemplate: RestTemplate, - appFhirConfig: AppFhirConfig - ): IConsentService { - return GicsConsentService( - gIcsConfigProperties, - retryTemplate, - restTemplate, - appFhirConfig - ) - } + @ConditionalOnProperty(value = ["app.pseudonymizer"], havingValue = "GPAS") + @ConditionalOnMissingBean + @Bean + fun gPasConnectionCheckServiceOnDeprecatedProperty( + restTemplate: RestTemplate, + gPasConfigProperties: GPasConfigProperties, + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService { + return GPasConnectionCheckService( + restTemplate, + gPasConfigProperties, + connectionCheckUpdateProducer, + ) + } - @Conditional(GicsEnabledCondition::class) - @Bean - fun consentProcessor( - configProperties: AppConfigProperties, - gIcsConfigProperties: GIcsConfigProperties, - getObjectMapper: ObjectMapper, - appFhirConfig: AppFhirConfig, - gicsConsentService: IConsentService - ): ConsentProcessor { - return ConsentProcessor( - configProperties, - gIcsConfigProperties, - getObjectMapper, - appFhirConfig.fhirContext(), - gicsConsentService - ) - } + @Bean + fun jdbcConfiguration(): AbstractJdbcConfiguration { + return AppJdbcConfiguration() + } - @Conditional(GicsEnabledCondition::class) - @Bean - fun gIcsConnectionCheckService( - restTemplate: RestTemplate, - gIcsConfigProperties: GIcsConfigProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return GIcsConnectionCheckService( - restTemplate, - gIcsConfigProperties, - connectionCheckUpdateProducer - ) - } + @Conditional(GicsEnabledCondition::class) + @Bean + fun gicsConsentService( + gIcsConfigProperties: GIcsConfigProperties, + retryTemplate: RetryTemplate, + restTemplate: RestTemplate, + appFhirConfig: AppFhirConfig, + ): IConsentService { + return GicsConsentService(gIcsConfigProperties, retryTemplate, restTemplate, appFhirConfig) + } - @Bean - @ConditionalOnMissingBean - fun iGetConsentService(): IConsentService { - return MtbFileConsentService() - } + @Conditional(GicsEnabledCondition::class) + @Bean + fun consentProcessor( + configProperties: AppConfigProperties, + gIcsConfigProperties: GIcsConfigProperties, + getObjectMapper: ObjectMapper, + appFhirConfig: AppFhirConfig, + gicsConsentService: IConsentService, + ): ConsentProcessor { + return ConsentProcessor( + configProperties, + gIcsConfigProperties, + getObjectMapper, + appFhirConfig.fhirContext(), + gicsConsentService, + ) + } + @Conditional(GicsEnabledCondition::class) + @Bean + fun gIcsConnectionCheckService( + restTemplate: RestTemplate, + gIcsConfigProperties: GIcsConfigProperties, + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService { + return GIcsConnectionCheckService( + restTemplate, + gIcsConfigProperties, + connectionCheckUpdateProducer, + ) + } + + @Bean + @ConditionalOnMissingBean + fun iGetConsentService(): IConsentService { + return MtbFileConsentService() + } } 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.service"], havingValue = "gics") + @ConditionalOnProperty(name = ["app.consent.gics.uri"]) + class OnGicsServiceSelected { + // Just for Condition + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt index 2b5ff8f..052822e 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppFhirConfig.kt @@ -4,13 +4,9 @@ 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 + @Bean fun fhirContext(): FhirContext = fhirCtx +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt index 898982c..769faf3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppJdbcConfiguration.kt @@ -7,19 +7,13 @@ import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration @Configuration class AppJdbcConfiguration : AbstractJdbcConfiguration() { - override fun userConverters(): MutableList<*> { - return mutableListOf(StringToFingerprintConverter(), FingerprintToStringConverter()) - } + override fun userConverters(): MutableList<*> = mutableListOf(StringToFingerprintConverter(), FingerprintToStringConverter()) } class StringToFingerprintConverter : Converter<String, Fingerprint> { - override fun convert(source: String): Fingerprint { - return Fingerprint(source) - } + override fun convert(source: String): Fingerprint = Fingerprint(source) } class FingerprintToStringConverter : Converter<Fingerprint, String> { - override fun convert(source: Fingerprint): String { - return source.value - } -}
\ No newline at end of file + override fun convert(source: Fingerprint): String = source.value +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt index 6551713..2f89dea 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppKafkaConfiguration.kt @@ -45,14 +45,11 @@ import org.springframework.retry.support.RetryTemplate import reactor.core.publisher.Sinks @Configuration -@EnableConfigurationProperties( - value = [KafkaProperties::class] -) +@EnableConfigurationProperties(value = [KafkaProperties::class]) @ConditionalOnProperty(value = ["app.kafka.servers"]) @ConditionalOnMissingBean(MtbFileSender::class) @Order(-5) class AppKafkaConfiguration { - private val logger = LoggerFactory.getLogger(AppKafkaConfiguration::class.java) @Bean @@ -60,7 +57,7 @@ class AppKafkaConfiguration { kafkaTemplate: KafkaTemplate<String, String>, kafkaProperties: KafkaProperties, retryTemplate: RetryTemplate, - objectMapper: ObjectMapper + objectMapper: ObjectMapper, ): MtbFileSender { logger.info("Selected 'KafkaMtbFileSender'") return KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper) @@ -70,7 +67,7 @@ class AppKafkaConfiguration { fun kafkaResponseListenerContainer( consumerFactory: ConsumerFactory<String, String>, kafkaProperties: KafkaProperties, - kafkaResponseProcessor: KafkaResponseProcessor + kafkaResponseProcessor: KafkaResponseProcessor, ): KafkaMessageListenerContainer<String, String> { val containerProperties = ContainerProperties(kafkaProperties.outputResponseTopic) containerProperties.messageListener = kafkaResponseProcessor @@ -80,17 +77,15 @@ class AppKafkaConfiguration { @Bean fun kafkaResponseProcessor( applicationEventPublisher: ApplicationEventPublisher, - objectMapper: ObjectMapper - ): KafkaResponseProcessor { - return KafkaResponseProcessor(applicationEventPublisher, objectMapper) - } + objectMapper: ObjectMapper, + ): KafkaResponseProcessor = KafkaResponseProcessor(applicationEventPublisher, objectMapper) @Bean @ConditionalOnProperty(value = ["app.kafka.input-topic"]) fun kafkaInputListenerContainer( consumerFactory: ConsumerFactory<String, String>, kafkaProperties: KafkaProperties, - kafkaInputListener: KafkaInputListener + kafkaInputListener: KafkaInputListener, ): KafkaMessageListenerContainer<String, String> { val containerProperties = ContainerProperties(kafkaProperties.inputTopic) containerProperties.messageListener = kafkaInputListener @@ -102,17 +97,16 @@ class AppKafkaConfiguration { fun kafkaInputListener( requestProcessor: RequestProcessor, objectMapper: ObjectMapper, - consentEvaluator: ConsentEvaluator - ): KafkaInputListener { - return KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) - } + consentEvaluator: ConsentEvaluator, + ): KafkaInputListener = KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) @Bean fun kafkaConnectionCheckService( consumerFactory: ConsumerFactory<String, String>, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return KafkaConnectionCheckService(consumerFactory.createConsumer(), connectionCheckUpdateProducer) - } - + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService = + KafkaConnectionCheckService( + consumerFactory.createConsumer(), + connectionCheckUpdateProducer, + ) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt index 62c25bc..565209e 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppRestConfiguration.kt @@ -37,16 +37,11 @@ import org.springframework.web.client.RestTemplate import reactor.core.publisher.Sinks @Configuration -@EnableConfigurationProperties( - value = [ - RestTargetProperties::class - ] -) +@EnableConfigurationProperties(value = [RestTargetProperties::class]) @ConditionalOnProperty(value = ["app.rest.uri"]) @ConditionalOnMissingBean(MtbFileSender::class) @Order(-10) class AppRestConfiguration { - private val logger = LoggerFactory.getLogger(AppRestConfiguration::class.java) @Bean @@ -64,10 +59,11 @@ class AppRestConfiguration { fun restConnectionCheckService( restTemplate: RestTemplate, restTargetProperties: RestTargetProperties, - connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> - ): ConnectionCheckService { - return RestConnectionCheckService(restTemplate, restTargetProperties, connectionCheckUpdateProducer) - } - + connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, + ): ConnectionCheckService = + RestConnectionCheckService( + restTemplate, + restTargetProperties, + connectionCheckUpdateProducer, + ) } - diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt index b2bd044..d44303b 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/AppSecurityConfiguration.kt @@ -22,6 +22,7 @@ package dev.dnpm.etl.processor.config import dev.dnpm.etl.processor.security.UserRole import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.security.UserRoleService +import java.util.* import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.EnableConfigurationProperties @@ -41,158 +42,146 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.security.web.SecurityFilterChain -import java.util.* - private const val LOGIN_PATH = "/login" @Configuration -@EnableConfigurationProperties( - value = [ - SecurityConfigProperties::class - ] -) +@EnableConfigurationProperties(value = [SecurityConfigProperties::class]) @ConditionalOnProperty(value = ["app.security.admin-user"]) @EnableWebSecurity -class AppSecurityConfiguration( - private val securityConfigProperties: SecurityConfigProperties -) { +class AppSecurityConfiguration(private val securityConfigProperties: SecurityConfigProperties) { - private val logger = LoggerFactory.getLogger(AppSecurityConfiguration::class.java) + private val logger = LoggerFactory.getLogger(AppSecurityConfiguration::class.java) - @Bean - fun userDetailsService(passwordEncoder: PasswordEncoder): InMemoryUserDetailsManager { - val adminUser = if (securityConfigProperties.adminUser.isNullOrBlank()) { - logger.warn("Using random Admin User: admin") - "admin" + @Bean + fun userDetailsService(passwordEncoder: PasswordEncoder): InMemoryUserDetailsManager { + val adminUser = + if (securityConfigProperties.adminUser.isNullOrBlank()) { + logger.warn("Using random Admin User: admin") + "admin" } else { - securityConfigProperties.adminUser + securityConfigProperties.adminUser } - val adminPassword = if (securityConfigProperties.adminPassword.isNullOrBlank()) { - val random = UUID.randomUUID().toString() - logger.warn("Using random Admin Passwort: {}", random) - passwordEncoder.encode(random) + val adminPassword = + if (securityConfigProperties.adminPassword.isNullOrBlank()) { + val random = UUID.randomUUID().toString() + logger.warn("Using random Admin Passwort: {}", random) + passwordEncoder.encode(random) } else { - securityConfigProperties.adminPassword + securityConfigProperties.adminPassword } - val user: UserDetails = User.withUsername(adminUser) - .password(adminPassword) - .roles("ADMIN") - .build() - - return InMemoryUserDetailsManager(user) - } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") - fun filterChainOidc( - http: HttpSecurity, - passwordEncoder: PasswordEncoder, - userRoleRepository: UserRoleRepository, - sessionRegistry: SessionRegistry - ): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize("/configs/**", hasRole("ADMIN")) - authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) - authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) - authorize("/report/**", hasAnyRole("ADMIN", "USER")) - authorize("*.css", permitAll) - authorize("*.ico", permitAll) - authorize("*.jpeg", permitAll) - authorize("*.js", permitAll) - authorize("*.svg", permitAll) - authorize("*.css", permitAll) - authorize("/login/**", permitAll) - authorize(anyRequest, permitAll) - } - httpBasic { - realmName = "ETL-Processor" - } - formLogin { - loginPage = LOGIN_PATH - } - oauth2Login { - loginPage = LOGIN_PATH - } - sessionManagement { - sessionConcurrency { - maximumSessions = 1 - expiredUrl = "$LOGIN_PATH?expired" - } - sessionFixation { - newSession() - } - } - csrf { disable() } + val user: UserDetails = + User.withUsername(adminUser).password(adminPassword).roles("ADMIN").build() + + return InMemoryUserDetailsManager(user) + } + + @Bean + @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") + fun filterChainOidc( + http: HttpSecurity, + passwordEncoder: PasswordEncoder, + userRoleRepository: UserRoleRepository, + sessionRegistry: SessionRegistry, + ): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/configs/**", hasRole("ADMIN")) + authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) + authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN", "USER")) + authorize("/report/**", hasAnyRole("ADMIN", "USER")) + authorize("*.css", permitAll) + authorize("*.ico", permitAll) + authorize("*.jpeg", permitAll) + authorize("*.js", permitAll) + authorize("*.svg", permitAll) + authorize("*.css", permitAll) + authorize("/login/**", permitAll) + authorize(anyRequest, permitAll) + } + httpBasic { realmName = "ETL-Processor" } + formLogin { loginPage = LOGIN_PATH } + oauth2Login { loginPage = LOGIN_PATH } + sessionManagement { + sessionConcurrency { + maximumSessions = 1 + expiredUrl = "$LOGIN_PATH?expired" } - return http.build() + sessionFixation { newSession() } + } + csrf { disable() } } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") - fun grantedAuthoritiesMapper( - userRoleRepository: UserRoleRepository, - appSecurityConfigProperties: SecurityConfigProperties - ): GrantedAuthoritiesMapper { - return GrantedAuthoritiesMapper { grantedAuthority -> - grantedAuthority.filterIsInstance<OidcUserAuthority>() - .onEach { - val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) - if (userRole.isEmpty) { - userRoleRepository.save( - UserRole( - null, - it.userInfo.preferredUsername, - appSecurityConfigProperties.defaultNewUserRole - ) - ) - } - } - .map { - val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) - SimpleGrantedAuthority("ROLE_${userRole.get().role.toString().uppercase()}") - } - } - } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "false", matchIfMissing = true) - fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain { - http { - authorizeHttpRequests { - authorize("/configs/**", hasRole("ADMIN")) - authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN")) - authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN")) - authorize("/report/**", hasRole("ADMIN")) - authorize(anyRequest, permitAll) + return http.build() + } + + @Bean + @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") + fun grantedAuthoritiesMapper( + userRoleRepository: UserRoleRepository, + appSecurityConfigProperties: SecurityConfigProperties, + ): GrantedAuthoritiesMapper { + return GrantedAuthoritiesMapper { grantedAuthority -> + grantedAuthority + .filterIsInstance<OidcUserAuthority>() + .onEach { + val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) + if (userRole.isEmpty) { + userRoleRepository.save( + UserRole( + null, + it.userInfo.preferredUsername, + appSecurityConfigProperties.defaultNewUserRole, + ) + ) } - httpBasic { - realmName = "ETL-Processor" - } - formLogin { - loginPage = LOGIN_PATH - } - csrf { disable() } - } - return http.build() + } + .map { + val userRole = userRoleRepository.findByUsername(it.userInfo.preferredUsername) + SimpleGrantedAuthority("ROLE_${userRole.get().role.toString().uppercase()}") + } } - - @Bean - fun sessionRegistry(): SessionRegistry { - return SessionRegistryImpl() - } - - @Bean - fun passwordEncoder(): PasswordEncoder { - return PasswordEncoderFactories.createDelegatingPasswordEncoder() - } - - @Bean - @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") - fun userRoleService(userRoleRepository: UserRoleRepository, sessionRegistry: SessionRegistry): UserRoleService { - return UserRoleService(userRoleRepository, sessionRegistry) + } + + @Bean + @ConditionalOnProperty( + value = ["app.security.enable-oidc"], + havingValue = "false", + matchIfMissing = true, + ) + fun filterChain(http: HttpSecurity, passwordEncoder: PasswordEncoder): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize("/configs/**", hasRole("ADMIN")) + authorize("/mtbfile/**", hasAnyRole("MTBFILE", "ADMIN")) + authorize("/mtb/**", hasAnyRole("MTBFILE", "ADMIN")) + authorize("/report/**", hasRole("ADMIN")) + authorize(anyRequest, permitAll) + } + httpBasic { realmName = "ETL-Processor" } + formLogin { loginPage = LOGIN_PATH } + csrf { disable() } } + return http.build() + } + + @Bean + fun sessionRegistry(): SessionRegistry { + return SessionRegistryImpl() + } + + @Bean + fun passwordEncoder(): PasswordEncoder { + return PasswordEncoderFactories.createDelegatingPasswordEncoder() + } + + @Bean + @ConditionalOnProperty(value = ["app.security.enable-oidc"], havingValue = "true") + fun userRoleService( + userRoleRepository: UserRoleRepository, + sessionRegistry: SessionRegistry, + ): UserRoleService { + return UserRoleService(userRoleRepository, sessionRegistry) + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt index 5469b1b..48163a1 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceDeserializer.kt @@ -1,18 +1,19 @@ package dev.dnpm.etl.processor.config import com.fasterxml.jackson.core.JsonParser - import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import org.hl7.fhir.r4.model.Consent class ConsentResourceDeserializer : JsonDeserializer<Consent>() { - override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Consent { - + override fun deserialize( + p: JsonParser?, + ctxt: DeserializationContext?, + ): Consent { val jsonNode = p?.readValueAsTree<JsonNode>() val json = jsonNode?.toString() return JacksonConfig.fhirContext().newJsonParser().parseResource(json) as Consent } -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt index 812ce44..b4f29a4 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/ConsentResourceSerializer.kt @@ -7,9 +7,11 @@ import org.hl7.fhir.r4.model.Consent class ConsentResourceSerializer : JsonSerializer<Consent>() { override fun serialize( - value: Consent, gen: JsonGenerator, serializers: SerializerProvider + value: Consent, + gen: JsonGenerator, + serializers: SerializerProvider, ) { val json = JacksonConfig.fhirContext().newJsonParser().encodeResourceToString(value) gen.writeRawValue(json) } -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt index 2ae0dd3..45a3d93 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/FhirResourceModule.kt @@ -1,6 +1,5 @@ package dev.dnpm.etl.processor.config - import com.fasterxml.jackson.databind.module.SimpleModule import org.hl7.fhir.r4.model.Consent @@ -9,4 +8,4 @@ class FhirResourceModule : SimpleModule() { addSerializer(Consent::class.java, ConsentResourceSerializer()) addDeserializer(Consent::class.java, ConsentResourceDeserializer()) } -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt index 282f69e..2480de8 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/config/JacksonConfig.kt @@ -2,33 +2,27 @@ package dev.dnpm.etl.processor.config import ca.uhn.fhir.context.FhirContext import com.fasterxml.jackson.annotation.JsonInclude -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jdk8.Jdk8Module import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration @Configuration class JacksonConfig { - companion object { var fhirContext: FhirContext = FhirContext.forR4() - @JvmStatic - fun fhirContext(): FhirContext { - return fhirContext - } + @JvmStatic fun fhirContext(): FhirContext = fhirContext } @Bean - fun objectMapper(): ObjectMapper = ObjectMapper().registerModule(FhirResourceModule()) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .registerModule( - JavaTimeModule() - ) - .registerModule( - Jdk8Module() - ) - .setSerializationInclusion(JsonInclude.Include.NON_NULL) + fun objectMapper(): ObjectMapper = + ObjectMapper() + .registerModule(FhirResourceModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .registerModule(JavaTimeModule()) + .registerModule(Jdk8Module()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt b/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt index 195346d..58f647f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/consent/ConsentEvaluator.kt @@ -24,38 +24,35 @@ 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 - */ +/** Evaluates consent using provided consent service and file based consent information */ @Service class ConsentEvaluator( - private val consentService: IConsentService + 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 + 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 + 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 - } +data class ConsentEvaluation( + private val ttpConsentStatus: TtpConsentStatus, + private val consentGiven: Boolean, +) { + /** Checks if any required consent is present */ + fun hasConsent(): Boolean = consentGiven - /** - * Returns the consent status - */ + /** Returns the consent status */ fun getStatus(): TtpConsentStatus { if (ttpConsentStatus == TtpConsentStatus.UNKNOWN_CHECK_FILE) { // in case ttp check is disabled - we propagate rejected status anyway 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 d53eb7e..2f6b2bb 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/KafkaInputListener.kt @@ -36,7 +36,7 @@ import java.nio.charset.Charset class KafkaInputListener( private val requestProcessor: RequestProcessor, private val consentEvaluator: ConsentEvaluator, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) : MessageListener<String, String> { private val logger = LoggerFactory.getLogger(KafkaInputListener::class.java) @@ -45,29 +45,40 @@ class KafkaInputListener( MediaType.APPLICATION_JSON_VALUE -> handleDnpmV2Message(record) CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE -> handleDnpmV2Message(record) else -> { - /* ignore other messages */ + // ignore other messages } } } private fun guessMimeType(record: ConsumerRecord<String, String>): String? { - if (record.headers().headers("contentType").toList().isEmpty()) { + if (record + .headers() + .headers("contentType") + .toList() + .isEmpty() + ) { // Fallback if no contentType set (old behavior) return MediaType.APPLICATION_JSON_VALUE } - return record.headers().headers("contentType")?.firstOrNull()?.value()?.toString(Charset.forName("UTF-8")) + return record + .headers() + .headers("contentType") + ?.firstOrNull() + ?.value() + ?.toString(Charset.forName("UTF-8")) } private fun handleDnpmV2Message(record: ConsumerRecord<String, String>) { val mtbFile = objectMapper.readValue(record.value(), Mtb::class.java) val patientId = PatientId(mtbFile.patient.id) val firstRequestIdHeader = record.headers().headers("requestId")?.firstOrNull() - val requestId = if (null != firstRequestIdHeader) { - RequestId(String(firstRequestIdHeader.value())) - } else { - RequestId("") - } + val requestId = + if (null != firstRequestIdHeader) { + RequestId(String(firstRequestIdHeader.value())) + } else { + RequestId("") + } if (consentEvaluator.check(mtbFile).hasConsent()) { logger.debug("Accepted MTB File for processing") @@ -81,13 +92,8 @@ class KafkaInputListener( if (requestId.isBlank()) { requestProcessor.processDeletion(patientId, TtpConsentStatus.UNKNOWN_CHECK_FILE) } else { - requestProcessor.processDeletion( - patientId, - requestId, - TtpConsentStatus.UNKNOWN_CHECK_FILE - ) + requestProcessor.processDeletion(patientId, requestId, TtpConsentStatus.UNKNOWN_CHECK_FILE) } } } - } 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 e154536..5a43242 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/input/MtbFileRestController.kt @@ -34,34 +34,36 @@ import org.springframework.web.bind.annotation.* @RequestMapping(path = ["mtbfile", "mtb"]) class MtbFileRestController( private val requestProcessor: RequestProcessor, - private val consentEvaluator: ConsentEvaluator + private val consentEvaluator: ConsentEvaluator, ) { - private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) + private val logger = LoggerFactory.getLogger(MtbFileRestController::class.java) - @GetMapping - fun info(): ResponseEntity<String> { - return ResponseEntity.ok("Test") - } - - @PostMapping(consumes = [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE]) - fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> { - 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() - } + @GetMapping + fun info(): ResponseEntity<String> { + return ResponseEntity.ok("Test") + } - @DeleteMapping(path = ["{patientId}"]) - fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> { - logger.debug("Accepted patient ID to process deletion") - requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE) - return ResponseEntity.accepted().build() + @PostMapping( + consumes = + [MediaType.APPLICATION_JSON_VALUE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE] + ) + fun mtbFile(@RequestBody mtbFile: Mtb): ResponseEntity<Unit> { + 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() + } + @DeleteMapping(path = ["{patientId}"]) + fun deleteData(@PathVariable patientId: String): ResponseEntity<Unit> { + logger.debug("Accepted patient ID to process deletion") + requestProcessor.processDeletion(PatientId(patientId), TtpConsentStatus.UNKNOWN_CHECK_FILE) + return ResponseEntity.accepted().build() + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt index 6e97865..085d0a3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckService.kt @@ -17,13 +17,15 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - package dev.dnpm.etl.processor.monitoring import dev.dnpm.etl.processor.config.GIcsConfigProperties import dev.dnpm.etl.processor.config.GPasConfigProperties import dev.dnpm.etl.processor.config.RestTargetProperties import jakarta.annotation.PostConstruct +import java.time.Instant +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration import org.apache.kafka.clients.consumer.Consumer import org.apache.kafka.common.errors.TimeoutException import org.slf4j.LoggerFactory @@ -33,254 +35,288 @@ import org.springframework.scheduling.annotation.Scheduled import org.springframework.web.client.RestTemplate import org.springframework.web.util.UriComponentsBuilder import reactor.core.publisher.Sinks -import java.time.Instant -import kotlin.time.Duration.Companion.seconds -import kotlin.time.toJavaDuration fun interface ConnectionCheckService { - fun connectionAvailable(): ConnectionCheckResult - + fun connectionAvailable(): ConnectionCheckResult } interface OutputConnectionCheckService : ConnectionCheckService sealed class ConnectionCheckResult { - abstract val available: Boolean + abstract val available: Boolean - abstract val timestamp: Instant + abstract val timestamp: Instant - abstract val lastChange: Instant + abstract val lastChange: Instant - data class KafkaConnectionCheckResult( - override val available: Boolean, - override val timestamp: Instant, - override val lastChange: Instant - ) : ConnectionCheckResult() + data class KafkaConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant, + ) : ConnectionCheckResult() - data class RestConnectionCheckResult( - override val available: Boolean, - override val timestamp: Instant, - override val lastChange: Instant - ) : ConnectionCheckResult() + data class RestConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant, + ) : ConnectionCheckResult() - data class GPasConnectionCheckResult( - override val available: Boolean, - override val timestamp: Instant, - override val lastChange: Instant - ) : ConnectionCheckResult() + data class GPasConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant, + ) : ConnectionCheckResult() - data class GIcsConnectionCheckResult( - override val available: Boolean, - override val timestamp: Instant, - override val lastChange: Instant - ) : ConnectionCheckResult() + data class GIcsConnectionCheckResult( + override val available: Boolean, + override val timestamp: Instant, + override val lastChange: Instant, + ) : ConnectionCheckResult() } class KafkaConnectionCheckService( private val consumer: Consumer<String, String>, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : OutputConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - private var result = ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now()) - - @PostConstruct - @Scheduled(cron = "0 * * * * *") - fun check() { - result = try { - val available = null != consumer.listTopics(5.seconds.toJavaDuration()) - ConnectionCheckResult.KafkaConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + private var result = + ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = + try { + val available = null != consumer.listTopics(5.seconds.toJavaDuration()) + ConnectionCheckResult.KafkaConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: TimeoutException) { - logger.error("Connection-Timeout error: {}", ex.message) - ConnectionCheckResult.KafkaConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Timeout error: {}", ex.message) + ConnectionCheckResult.KafkaConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { + result.lastChange + } else { + Instant.now() + }, + ) } - connectionCheckUpdateProducer.emitNext( - result, - Sinks.EmitFailureHandler.FAIL_FAST - ) - } - - override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult { - return this.result - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult { + return this.result + } } class RestConnectionCheckService( private val restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : OutputConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - private var result = ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now()) - - @PostConstruct - @Scheduled(cron = "0 * * * * *") - fun check() { - result = try { - val statusCode = restTemplate.getForEntity( - UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString()) - .pathSegment("mtb") - .pathSegment("kaplan-meier") - .pathSegment("config") - .toUriString(), - String::class.java - ).statusCode - val available = statusCode == HttpStatus.OK - - if (available.not()) { - logger.error("Invalid response code {}, expected HTTP status 200", statusCode) - } - ConnectionCheckResult.RestConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + private var result = + ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = + try { + val statusCode = + restTemplate + .getForEntity( + UriComponentsBuilder.fromUriString(restTargetProperties.uri.toString()) + .pathSegment("mtb") + .pathSegment("kaplan-meier") + .pathSegment("config") + .toUriString(), + String::class.java, + ) + .statusCode + val available = statusCode == HttpStatus.OK + + if (available.not()) { + logger.error("Invalid response code {}, expected HTTP status 200", statusCode) + } + ConnectionCheckResult.RestConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: Exception) { - logger.error("Connection-Check error: {}", ex.message) - ConnectionCheckResult.RestConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Check error: {}", ex.message) + ConnectionCheckResult.RestConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { + result.lastChange + } else { + Instant.now() + }, + ) } - connectionCheckUpdateProducer.emitNext( - result, - Sinks.EmitFailureHandler.FAIL_FAST - ) - } - - override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult { - return this.result - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + + override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult { + return this.result + } } class GPasConnectionCheckService( private val restTemplate: RestTemplate, private val gPasConfigProperties: GPasConfigProperties, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : ConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - private var result = ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now()) - - @PostConstruct - @Scheduled(cron = "0 * * * * *") - fun check() { - result = try { - val uri = UriComponentsBuilder.fromUriString( - gPasConfigProperties.uri.toString()).path("/metadata").build().toUri() - - val headers = HttpHeaders() - headers.contentType = MediaType.APPLICATION_JSON - if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) { - headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password) - } - - val statusCode = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity<Void>(headers), - Void::class.java - ).statusCode - val available = statusCode == HttpStatus.OK - - if (available.not()) { - logger.error("Invalid response code {}, expected HTTP status 200", statusCode) - } - ConnectionCheckResult.GPasConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + private var result = + ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = + try { + val uri = + UriComponentsBuilder.fromUriString(gPasConfigProperties.uri.toString()) + .path("/metadata") + .build() + .toUri() + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + if ( + !gPasConfigProperties.username.isNullOrBlank() && + !gPasConfigProperties.password.isNullOrBlank() + ) { + headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password) + } + + val statusCode = + restTemplate + .exchange(uri, HttpMethod.GET, HttpEntity<Void>(headers), Void::class.java) + .statusCode + val available = statusCode == HttpStatus.OK + + if (available.not()) { + logger.error("Invalid response code {}, expected HTTP status 200", statusCode) + } + ConnectionCheckResult.GPasConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: Exception) { - logger.error("Connection-Check error: {}", ex.message) - ConnectionCheckResult.GPasConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Check error: {}", ex.message) + ConnectionCheckResult.GPasConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { + result.lastChange + } else { + Instant.now() + }, + ) } - connectionCheckUpdateProducer.emitNext( - result, - Sinks.EmitFailureHandler.FAIL_FAST - ) - } - - override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult { - return this.result - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + + override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult { + return this.result + } } class GIcsConnectionCheckService( private val restTemplate: RestTemplate, private val gIcsConfigProperties: GIcsConfigProperties, @param:Qualifier("connectionCheckUpdateProducer") - private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult> + private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>, ) : ConnectionCheckService { - private val logger = LoggerFactory.getLogger(javaClass) - private var result = ConnectionCheckResult.GIcsConnectionCheckResult(false, Instant.now(), Instant.now()) - - @PostConstruct - @Scheduled(cron = "0 * * * * *") - fun check() { - result = try { - - val uri = UriComponentsBuilder.fromUriString( - gIcsConfigProperties.uri.toString()).path("/metadata").build().toUri() - - val headers = HttpHeaders() - headers.contentType = MediaType.APPLICATION_JSON - if (!gIcsConfigProperties.username.isNullOrBlank() && !gIcsConfigProperties.password.isNullOrBlank()) { - headers.setBasicAuth(gIcsConfigProperties.username, gIcsConfigProperties.password) - } - - val statusCode = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity<Void>(headers), - Void::class.java - ).statusCode - val available = statusCode == HttpStatus.OK - - if (available.not()) { - logger.error("Invalid response code {}, expected HTTP status 200", statusCode) - } - ConnectionCheckResult.GIcsConnectionCheckResult( - available, - Instant.now(), - if (result.available == available) { result.lastChange } else { Instant.now() } - ) + private val logger = LoggerFactory.getLogger(javaClass) + private var result = + ConnectionCheckResult.GIcsConnectionCheckResult(false, Instant.now(), Instant.now()) + + @PostConstruct + @Scheduled(cron = "0 * * * * *") + fun check() { + result = + try { + + val uri = + UriComponentsBuilder.fromUriString(gIcsConfigProperties.uri.toString()) + .path("/metadata") + .build() + .toUri() + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_JSON + if ( + !gIcsConfigProperties.username.isNullOrBlank() && + !gIcsConfigProperties.password.isNullOrBlank() + ) { + headers.setBasicAuth(gIcsConfigProperties.username, gIcsConfigProperties.password) + } + + val statusCode = + restTemplate + .exchange(uri, HttpMethod.GET, HttpEntity<Void>(headers), Void::class.java) + .statusCode + val available = statusCode == HttpStatus.OK + + if (available.not()) { + logger.error("Invalid response code {}, expected HTTP status 200", statusCode) + } + ConnectionCheckResult.GIcsConnectionCheckResult( + available, + Instant.now(), + if (result.available == available) { + result.lastChange + } else { + Instant.now() + }, + ) } catch (ex: Exception) { - logger.error("Connection-Check error: {}", ex.message) - ConnectionCheckResult.GIcsConnectionCheckResult( - false, - Instant.now(), - if (!result.available) { result.lastChange } else { Instant.now() } - ) + logger.error("Connection-Check error: {}", ex.message) + ConnectionCheckResult.GIcsConnectionCheckResult( + false, + Instant.now(), + if (!result.available) { + result.lastChange + } else { + Instant.now() + }, + ) } - connectionCheckUpdateProducer.emitNext( - result, - Sinks.EmitFailureHandler.FAIL_FAST - ) - } - - override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult { - return this.result - } + connectionCheckUpdateProducer.emitNext(result, Sinks.EmitFailureHandler.FAIL_FAST) + } + + override fun connectionAvailable(): ConnectionCheckResult.GIcsConnectionCheckResult { + return this.result + } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt index dd5c44a..c54aa7a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/ReportService.kt @@ -30,61 +30,53 @@ import dev.dnpm.etl.processor.monitoring.ReportService.Issue import dev.dnpm.etl.processor.monitoring.ReportService.Severity import java.util.* -class ReportService( - private val objectMapper: ObjectMapper -) { +class ReportService(private val objectMapper: ObjectMapper) { - fun deserialize(dataQualityReport: String?): List<Issue> { - if (dataQualityReport.isNullOrBlank()) { - return listOf() - } - return try { - objectMapper - .readValue(dataQualityReport, DataQualityReport::class.java) - .issues - .sortedBy { it.severity } - } catch (e: Exception) { - val otherIssue = - Issue(Severity.ERROR, "Not parsable data quality report '$dataQualityReport'") - return when (e) { - is JsonMappingException -> listOf(otherIssue) - is JsonParseException -> listOf(otherIssue) - else -> throw e - } - } + fun deserialize(dataQualityReport: String?): List<Issue> { + if (dataQualityReport.isNullOrBlank()) { + return listOf() } + return try { + objectMapper.readValue(dataQualityReport, DataQualityReport::class.java).issues.sortedBy { + it.severity + } + } catch (e: Exception) { + val otherIssue = + Issue(Severity.ERROR, "Not parsable data quality report '$dataQualityReport'") + return when (e) { + is JsonMappingException -> listOf(otherIssue) + is JsonParseException -> listOf(otherIssue) + else -> throw e + } + } + } + @JsonIgnoreProperties(ignoreUnknown = true) + private data class DataQualityReport( + @param:JsonProperty(value = "issues") val issues: List<Issue> + ) - @JsonIgnoreProperties(ignoreUnknown = true) - private data class DataQualityReport( - @param:JsonProperty(value = "issues") - val issues: List<Issue> - ) - - @JsonIgnoreProperties(ignoreUnknown = true) - data class Issue( - @param:JsonProperty(value = "severity") - val severity: Severity, - @param:JsonProperty(value = "message") - @param:JsonAlias("details") - val message: String, - @param:JsonProperty(value = "path") - val path: Optional<String> = Optional.empty() - ) + @JsonIgnoreProperties(ignoreUnknown = true) + data class Issue( + @param:JsonProperty(value = "severity") val severity: Severity, + @param:JsonProperty(value = "message") @param:JsonAlias("details") val message: String, + @param:JsonProperty(value = "path") val path: Optional<String> = Optional.empty(), + ) - enum class Severity(@JsonValue val value: String) { - FATAL("fatal"), - ERROR("error"), - WARNING("warning"), - INFO("info") - } + enum class Severity(@JsonValue val value: String) { + FATAL("fatal"), + ERROR("error"), + WARNING("warning"), + INFO("info"), + } } fun List<Issue>.asRequestStatus(): RequestStatus { - val severity = this.minOfOrNull { it.severity } - return when (severity) { - Severity.FATAL, Severity.ERROR -> RequestStatus.ERROR - Severity.WARNING -> RequestStatus.WARNING - else -> RequestStatus.SUCCESS - } -}
\ No newline at end of file + val severity = this.minOfOrNull { it.severity } + return when (severity) { + Severity.FATAL, + Severity.ERROR -> RequestStatus.ERROR + Severity.WARNING -> RequestStatus.WARNING + else -> RequestStatus.SUCCESS + } +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt index f2509dd..71731f1 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/Request.kt @@ -20,6 +20,9 @@ package dev.dnpm.etl.processor.monitoring import dev.dnpm.etl.processor.* +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* import org.springframework.data.annotation.Id import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable @@ -29,9 +32,6 @@ import org.springframework.data.relational.core.mapping.Embedded import org.springframework.data.relational.core.mapping.Table import org.springframework.data.repository.CrudRepository import org.springframework.data.repository.PagingAndSortingRepository -import java.time.Instant -import java.time.temporal.ChronoUnit -import java.util.* @Table("request") data class Request( @@ -39,46 +39,38 @@ data class Request( val uuid: RequestId = randomRequestId(), val patientPseudonym: PatientPseudonym, val pid: PatientId, - @Column("fingerprint") - val fingerprint: Fingerprint, + @Column("fingerprint") val fingerprint: Fingerprint, val type: RequestType, var status: RequestStatus, var processedAt: Instant = Instant.now(), - @Embedded.Nullable var report: Report? = null + @Embedded.Nullable var report: Report? = null, ) { - constructor( - uuid: RequestId, - patientPseudonym: PatientPseudonym, - pid: PatientId, - fingerprint: Fingerprint, - type: RequestType, - status: RequestStatus - ) : - this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now()) - - constructor( - uuid: RequestId, - patientPseudonym: PatientPseudonym, - pid: PatientId, - fingerprint: Fingerprint, - type: RequestType, - status: RequestStatus, - processedAt: Instant - ) : - this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt) - - fun isPendingUnknown(): Boolean { - return this.status == RequestStatus.UNKNOWN && this.processedAt.isBefore( - Instant.now().minus(10, ChronoUnit.MINUTES) - ) - } + constructor( + uuid: RequestId, + patientPseudonym: PatientPseudonym, + pid: PatientId, + fingerprint: Fingerprint, + type: RequestType, + status: RequestStatus, + ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, Instant.now()) + + constructor( + uuid: RequestId, + patientPseudonym: PatientPseudonym, + pid: PatientId, + fingerprint: Fingerprint, + type: RequestType, + status: RequestStatus, + processedAt: Instant, + ) : this(null, uuid, patientPseudonym, pid, fingerprint, type, status, processedAt) + + fun isPendingUnknown(): Boolean { + return this.status == RequestStatus.UNKNOWN && + this.processedAt.isBefore(Instant.now().minus(10, ChronoUnit.MINUTES)) + } } -@JvmRecord -data class Report( - val description: String, - val dataQualityReport: String = "" -) +@JvmRecord data class Report(val description: String, val dataQualityReport: String = "") @JvmRecord data class CountedState( @@ -86,34 +78,41 @@ data class CountedState( val status: RequestStatus, ) -interface RequestRepository : CrudRepository<Request, Long>, PagingAndSortingRepository<Request, Long> { - - fun findAllByPatientPseudonymOrderByProcessedAtDesc(patientId: PatientPseudonym): List<Request> - - fun findByUuidEquals(uuid: RequestId): Optional<Request> - - fun findRequestByPatientPseudonym(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request> - - @Query("SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;") - fun countStates(): List<CountedState> - - @Query( - "SELECT count(*) AS count, status FROM (" + - "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + - "WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " + - ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" - ) - fun findPatientUniqueStates(): List<CountedState> - - @Query("SELECT count(*) AS count, status FROM request WHERE type = 'DELETE' GROUP BY status ORDER BY status, count DESC;") - fun countDeleteStates(): List<CountedState> - - @Query( - "SELECT count(*) AS count, status FROM (" + - "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + - "WHERE type = 'DELETE'" + - ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" - ) - fun findPatientUniqueDeleteStates(): List<CountedState> - +interface RequestRepository : + CrudRepository<Request, Long>, PagingAndSortingRepository<Request, Long> { + + fun findAllByPatientPseudonymOrderByProcessedAtDesc(patientId: PatientPseudonym): List<Request> + + fun findByUuidEquals(uuid: RequestId): Optional<Request> + + fun findRequestByPatientPseudonym( + patientPseudonym: PatientPseudonym, + pageable: Pageable, + ): Page<Request> + + @Query( + "SELECT count(*) AS count, status FROM request WHERE type = 'MTB_FILE' GROUP BY status ORDER BY status, count DESC;" + ) + fun countStates(): List<CountedState> + + @Query( + "SELECT count(*) AS count, status FROM (" + + "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + + "WHERE type = 'MTB_FILE' AND status NOT IN ('DUPLICATION') " + + ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" + ) + fun findPatientUniqueStates(): List<CountedState> + + @Query( + "SELECT count(*) AS count, status FROM request WHERE type = 'DELETE' GROUP BY status ORDER BY status, count DESC;" + ) + fun countDeleteStates(): List<CountedState> + + @Query( + "SELECT count(*) AS count, status FROM (" + + "SELECT status, rank() OVER (PARTITION BY patient_pseudonym ORDER BY processed_at DESC) AS rank FROM request " + + "WHERE type = 'DELETE'" + + ") rank WHERE rank = 1 GROUP BY status ORDER BY status, count DESC;" + ) + fun findPatientUniqueDeleteStates(): List<CountedState> } 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 0c8adb1..5487a05 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestStatus.kt @@ -19,11 +19,13 @@ package dev.dnpm.etl.processor.monitoring -enum class RequestStatus(val value: String) { +enum class RequestStatus( + val value: String, +) { SUCCESS("success"), WARNING("warning"), ERROR("error"), UNKNOWN("unknown"), DUPLICATION("duplication"), - NO_CONSENT("no-consent") -}
\ No newline at end of file + NO_CONSENT("no-consent"), +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt index cb43d7f..ef7f1e3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/monitoring/RequestType.kt @@ -19,7 +19,9 @@ package dev.dnpm.etl.processor.monitoring -enum class RequestType(val value: String) { +enum class RequestType( + val value: String, +) { MTB_FILE("mtb_file"), DELETE("delete"), -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt index ef46c0a..71e4a78 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSender.kt @@ -34,9 +34,8 @@ class KafkaMtbFileSender( private val kafkaTemplate: KafkaTemplate<String, String>, private val kafkaProperties: KafkaProperties, private val retryTemplate: RetryTemplate, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) : MtbFileSender { - private val logger = LoggerFactory.getLogger(KafkaMtbFileSender::class.java) override fun <T> send(request: MtbFileRequest<T>): MtbFileSender.Response { @@ -50,11 +49,13 @@ class KafkaMtbFileSender( ) record.headers().add("requestId", request.requestId.value.toByteArray()) when (request) { - is DnpmV2MtbFileRequest -> record.headers() - .add( - "contentType", - CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray() - ) + is DnpmV2MtbFileRequest -> + record + .headers() + .add( + "contentType", + CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray(), + ) } val result = kafkaTemplate.send(record) @@ -72,9 +73,7 @@ class KafkaMtbFileSender( } override fun send(request: DeleteRequest): MtbFileSender.Response { - val dummyMtbFile = Mtb.builder() - .metadata(MvhMetadata()) - .build() + val dummyMtbFile = Mtb.builder().metadata(MvhMetadata()).build() return try { return retryTemplate.execute<MtbFileSender.Response, Exception> { @@ -83,11 +82,8 @@ class KafkaMtbFileSender( kafkaProperties.outputTopic, key(request), objectMapper.writeValueAsString( - DnpmV2MtbFileRequest( - request.requestId, - dummyMtbFile - ) - ) + DnpmV2MtbFileRequest(request.requestId, dummyMtbFile), + ), ) record.headers().add("requestId", request.requestId.value.toByteArray()) val result = kafkaTemplate.send(record) @@ -104,15 +100,14 @@ class KafkaMtbFileSender( } } - override fun endpoint(): String { - return "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})" - } + override fun endpoint(): String = + "${this.kafkaProperties.servers} (${this.kafkaProperties.outputTopic}/${this.kafkaProperties.outputResponseTopic})" - private fun key(request: MtbRequest): String { - return when (request) { + private fun key(request: MtbRequest): String = + when (request) { is DnpmV2MtbFileRequest -> "{\"pid\": \"${request.content.patient.id}\"}" is DeleteRequest -> "{\"pid\": \"${request.patientId.value}\"}" - else -> throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}") + else -> + throw IllegalArgumentException("Unsupported request type: ${request::class.simpleName}") } - } } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt index 285ce07..c81b572 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbFileSender.kt @@ -29,18 +29,18 @@ interface MtbFileSender { fun endpoint(): String - data class Response(val status: RequestStatus, val body: String = "") + data class Response( + val status: RequestStatus, + val body: String = "", + ) } -fun Int.asRequestStatus(): RequestStatus { - return when (this) { +fun Int.asRequestStatus(): RequestStatus = + when (this) { 200 -> RequestStatus.SUCCESS 201 -> RequestStatus.WARNING - in 400 .. 999 -> RequestStatus.ERROR - else -> RequestStatus.UNKNOWN + in 400..999 -> RequestStatus.ERROR + else -> RequestStatus.UNKNOWN } -} -fun HttpStatusCode.asRequestStatus(): RequestStatus { - return this.value().asRequestStatus() -} +fun HttpStatusCode.asRequestStatus(): RequestStatus = this.value().asRequestStatus() diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt index 7512200..b228c4c 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/MtbRequest.kt @@ -36,14 +36,12 @@ sealed interface MtbFileRequest<out T> : MtbRequest { data class DnpmV2MtbFileRequest( override val requestId: RequestId, - override val content: Mtb + override val content: Mtb, ) : MtbFileRequest<Mtb> { - override fun patientPseudonym(): PatientPseudonym { - return PatientPseudonym(content.patient.id) - } + override fun patientPseudonym(): PatientPseudonym = PatientPseudonym(content.patient.id) } data class DeleteRequest( override val requestId: RequestId, - val patientId: PatientPseudonym + val patientId: PatientPseudonym, ) : MtbRequest diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt index 1e6a5a7..5aad133 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSender.kt @@ -30,26 +30,22 @@ class RestDipMtbFileSender( restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, retryTemplate: RetryTemplate, - reportService: ReportService + reportService: ReportService, ) : RestMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) { - - override fun sendUrl(): String { - return UriComponentsBuilder + override fun sendUrl(): String = + UriComponentsBuilder .fromUriString(restTargetProperties.uri.toString()) .pathSegment("mtb") .pathSegment("etl") .pathSegment("patient-record") .toUriString() - } - override fun deleteUrl(patientId: PatientPseudonym): String { - return UriComponentsBuilder + override fun deleteUrl(patientId: PatientPseudonym): String = + UriComponentsBuilder .fromUriString(restTargetProperties.uri.toString()) .pathSegment("mtb") .pathSegment("etl") .pathSegment("patient") .pathSegment(patientId.value) .toUriString() - } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt index ec6ff85..4120d4a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/output/RestMtbFileSender.kt @@ -38,9 +38,8 @@ abstract class RestMtbFileSender( private val restTemplate: RestTemplate, private val restTargetProperties: RestTargetProperties, private val retryTemplate: RetryTemplate, - private val reportService: ReportService + private val reportService: ReportService, ) : MtbFileSender { - private val logger = LoggerFactory.getLogger(RestMtbFileSender::class.java) abstract fun sendUrl(): String @@ -52,27 +51,29 @@ abstract class RestMtbFileSender( return retryTemplate.execute<MtbFileSender.Response, Exception> { val headers = getHttpHeaders(request) val entityReq = HttpEntity(request.content, headers) - val response = restTemplate.postForEntity( - sendUrl(), - entityReq, - String::class.java - ) + val response = restTemplate.postForEntity(sendUrl(), entityReq, String::class.java) if (!response.statusCode.is2xxSuccessful) { logger.warn("Error sending to remote system: {}", response.body) return@execute MtbFileSender.Response( reportService.deserialize(response.body).asRequestStatus(), - "Status-Code: ${response.statusCode.value()}" + "Status-Code: ${response.statusCode.value()}", ) } logger.debug("Sent file via RestMtbFileSender") - return@execute MtbFileSender.Response(reportService.deserialize(response.body).asRequestStatus(), response.body.orEmpty()) + return@execute MtbFileSender.Response( + reportService.deserialize(response.body).asRequestStatus(), + response.body.orEmpty(), + ) } } catch (e: IllegalArgumentException) { logger.error("Not a valid URI to export to: '{}'", restTargetProperties.uri!!) } catch (e: RestClientResponseException) { logger.info(restTargetProperties.uri!!.toString()) logger.error("Request data not accepted by remote system", e) - return MtbFileSender.Response(reportService.deserialize(e.responseBodyAsString).asRequestStatus(), e.responseBodyAsString) + return MtbFileSender.Response( + reportService.deserialize(e.responseBodyAsString).asRequestStatus(), + e.responseBodyAsString, + ) } return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung") } @@ -82,11 +83,7 @@ abstract class RestMtbFileSender( return retryTemplate.execute<MtbFileSender.Response, Exception> { val headers = getHttpHeaders(request) val entityReq = HttpEntity(null, headers) - restTemplate.delete( - deleteUrl(request.patientId), - entityReq, - String::class.java - ) + restTemplate.delete(deleteUrl(request.patientId), entityReq, String::class.java) logger.debug("Sent file via RestMtbFileSender") return@execute MtbFileSender.Response(RequestStatus.SUCCESS) } @@ -99,18 +96,17 @@ abstract class RestMtbFileSender( return MtbFileSender.Response(RequestStatus.ERROR, "Sonstiger Fehler bei der Übertragung") } - override fun endpoint(): String { - return this.restTargetProperties.uri.orEmpty() - } + override fun endpoint(): String = this.restTargetProperties.uri.orEmpty() private fun getHttpHeaders(request: MtbRequest): HttpHeaders { val username = restTargetProperties.username val password = restTargetProperties.password val headers = HttpHeaders() - headers.contentType = when (request) { - is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON - else -> MediaType.APPLICATION_JSON - } + headers.contentType = + when (request) { + is DnpmV2MtbFileRequest -> CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + else -> MediaType.APPLICATION_JSON + } if (username.isNullOrBlank() || password.isNullOrBlank()) { return headers @@ -119,5 +115,4 @@ abstract class RestMtbFileSender( headers.setBasicAuth(username, password) return headers } - } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt index dcb438f..90d867f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/AnonymizingGenerator.kt @@ -24,24 +24,17 @@ import org.apache.commons.codec.digest.DigestUtils import java.security.SecureRandom class AnonymizingGenerator : Generator { - companion object fun getSecureRandom() : SecureRandom { - return SecureRandom() - } + companion object - override fun generate(id: String): String { - return Base32().encodeAsString(DigestUtils.sha256(id)) - .substring(0..41) - .lowercase() - } + fun getSecureRandom(): SecureRandom = SecureRandom() + + override fun generate(id: String): String = Base32().encodeAsString(DigestUtils.sha256(id)).substring(0..41).lowercase() @OptIn(ExperimentalStdlibApi::class) override fun generateGenomDeTan(id: String): String { - val bytes = ByteArray(64 / 2) getSecureRandom().nextBytes(bytes) return bytes.joinToString("") { "%02x".format(it) } - } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt index 8215d23..089736c 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapPseudonymGenerator.kt @@ -8,19 +8,15 @@ class GpasSoapPseudonymGenerator( private val gpasCfg: GPasConfigProperties, private val retryTemplate: RetryTemplate, private val gpasSoapService: GpasSoapService, - private val appFhirConfig: AppFhirConfig + private val appFhirConfig: AppFhirConfig, ) : Generator { - - override fun generate(id: String): String { - return retryTemplate.execute<String, Exception> { + override fun generate(id: String): String = + retryTemplate.execute<String, Exception> { gpasSoapService.getOrCreatePseudonymFor(id, gpasCfg.patientDomain) } - } - override fun generateGenomDeTan(id: String): String { - return retryTemplate.execute<String, Exception> { + override fun generateGenomDeTan(id: String): String = + retryTemplate.execute<String, Exception> { gpasSoapService.createPseudonymsFor(id, gpasCfg.genomDeTanDomain, 1).first() } - } } - diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt index 0909924..f1121b8 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/GpasSoapService.kt @@ -8,15 +8,14 @@ import jakarta.xml.bind.annotation.XmlElementWrapper @WebService( name = "PSNManagerBeanService", - targetNamespace ="http://psn.ttp.ganimed.icmvc.emau.org/" + targetNamespace = "http://psn.ttp.ganimed.icmvc.emau.org/", ) interface GpasSoapService { - @WebMethod(operationName = "getOrCreatePseudonymFor") @WebResult(name = "psn") fun getOrCreatePseudonymFor( @WebParam(name = "value") value: String, - @WebParam(name = "domainName") domainName: String + @WebParam(name = "domainName") domainName: String, ): String @WebMethod(operationName = "createPseudonymsFor") @@ -25,7 +24,6 @@ interface GpasSoapService { fun createPseudonymsFor( @WebParam(name = "value") value: String, @WebParam(name = "domainName") domainName: String, - @WebParam(name = "number") minNumber: Int + @WebParam(name = "number") minNumber: Int, ): List<String> - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt index 96225a9..77ab87d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeService.kt @@ -25,22 +25,16 @@ import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties class PseudonymizeService( private val generator: Generator, - private val configProperties: PseudonymizeConfigProperties + private val configProperties: PseudonymizeConfigProperties, ) { - - fun patientPseudonym(patientId: PatientId): PatientPseudonym { - return when (generator) { + fun patientPseudonym(patientId: PatientId): PatientPseudonym = + when (generator) { is GpasPseudonymGenerator -> PatientPseudonym(generator.generate(patientId.value)) - else -> PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}") + else -> + PatientPseudonym("${configProperties.prefix}_${generator.generate(patientId.value)}") } - } - - fun genomDeTan(patientId: PatientId): String { - return generator.generateGenomDeTan(patientId.value) - } - fun prefix(): String { - return configProperties.prefix - } + fun genomDeTan(patientId: PatientId): String = generator.generateGenomDeTan(patientId.value) -}
\ No newline at end of file + fun prefix(): String = configProperties.prefix +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt index 8721cbe..48ac58a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/pseudonym/extensions.kt @@ -25,282 +25,246 @@ import dev.pcvolkmer.mv64e.mtb.Mtb import dev.pcvolkmer.mv64e.mtb.MvhMetadata import org.apache.commons.codec.digest.DigestUtils -/** Replaces patient ID with generated patient pseudonym - * - * @since 0.11.0 +/** + * Replaces patient ID with generated patient pseudonym * * @param pseudonymizeService The pseudonymizeService to be used * @return The MTB file containing patient pseudonymes + * @since 0.11.0 */ infix fun Mtb.pseudonymizeWith(pseudonymizeService: PseudonymizeService) { - val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value - - this.episodesOfCare?.forEach { it.patient?.id = patientPseudonym } - this.carePlans?.forEach { - it.patient.id = patientPseudonym - it.rebiopsyRequests?.forEach { it.patient?.id = patientPseudonym } - it.histologyReevaluationRequests?.forEach { it.patient?.id = patientPseudonym } - it.medicationRecommendations?.forEach { it.patient?.id = patientPseudonym } - it.studyEnrollmentRecommendations?.forEach { it.patient?.id = patientPseudonym } - it.procedureRecommendations?.forEach { it.patient?.id = patientPseudonym } - it.geneticCounselingRecommendation?.patient?.id = patientPseudonym - } - this.diagnoses?.forEach { it.patient?.id = patientPseudonym } - this.guidelineTherapies?.forEach { it.patient?.id = patientPseudonym } - this.guidelineProcedures?.forEach { it.patient?.id = patientPseudonym } - this.patient.id = patientPseudonym - this.claims?.forEach { it.patient?.id = patientPseudonym } - this.claimResponses?.forEach { it.patient?.id = patientPseudonym } - this.diagnoses?.forEach { it.patient?.id = patientPseudonym } - this.familyMemberHistories?.forEach { it.patient?.id = patientPseudonym } - this.histologyReports?.forEach { - it.patient.id = patientPseudonym - it.results.tumorMorphology?.patient?.id = patientPseudonym - it.results.tumorCellContent?.patient?.id = patientPseudonym - } - this.ngsReports?.forEach { - it.patient?.id = patientPseudonym - it.results?.simpleVariants?.forEach { it.patient?.id = patientPseudonym } - it.results?.copyNumberVariants?.forEach { it.patient?.id = patientPseudonym } - it.results?.dnaFusions?.forEach { it.patient?.id = patientPseudonym } - it.results?.rnaFusions?.forEach { it.patient?.id = patientPseudonym } - it.results?.tumorCellContent?.patient?.id = patientPseudonym - it.results?.brcaness?.patient?.id = patientPseudonym - it.results?.tmb?.patient?.id = patientPseudonym - it.results?.hrdScore?.patient?.id = patientPseudonym - } - this.ihcReports?.forEach { - it.patient?.id = patientPseudonym - it.results?.msiMmr?.forEach { it.patient?.id = patientPseudonym } - it.results?.proteinExpression?.forEach { it.patient?.id = patientPseudonym } - } - this.responses?.forEach { it.patient?.id = patientPseudonym } - this.specimens?.forEach { it.patient?.id = patientPseudonym } - this.priorDiagnosticReports?.forEach { it.patient?.id = patientPseudonym } - this.performanceStatus?.forEach { it.patient?.id = patientPseudonym } - this.systemicTherapies?.forEach { - it.history?.forEach { - it.patient?.id = patientPseudonym - } - } - this.followUps?.forEach { - it.patient?.id = patientPseudonym - } - - this.msiFindings?.forEach { it -> it.patient.id = patientPseudonym } - - this.metadata?.researchConsents?.forEach { it -> - val entry = it ?: return@forEach - if (entry.contains("patient")) { - // here we expect only a patient reference any other data like display - // need to be removed, since may contain unsecure data - entry.remove("patient") - entry["patient"] = mapOf("reference" to "Patient/$patientPseudonym") - } + val patientPseudonym = pseudonymizeService.patientPseudonym(PatientId(this.patient.id)).value + + this.episodesOfCare?.forEach { it.patient?.id = patientPseudonym } + this.carePlans?.forEach { + it.patient.id = patientPseudonym + it.rebiopsyRequests?.forEach { it.patient?.id = patientPseudonym } + it.histologyReevaluationRequests?.forEach { it.patient?.id = patientPseudonym } + it.medicationRecommendations?.forEach { it.patient?.id = patientPseudonym } + it.studyEnrollmentRecommendations?.forEach { it.patient?.id = patientPseudonym } + it.procedureRecommendations?.forEach { it.patient?.id = patientPseudonym } + it.geneticCounselingRecommendation?.patient?.id = patientPseudonym + } + this.diagnoses?.forEach { it.patient?.id = patientPseudonym } + this.guidelineTherapies?.forEach { it.patient?.id = patientPseudonym } + this.guidelineProcedures?.forEach { it.patient?.id = patientPseudonym } + this.patient.id = patientPseudonym + this.claims?.forEach { it.patient?.id = patientPseudonym } + this.claimResponses?.forEach { it.patient?.id = patientPseudonym } + this.diagnoses?.forEach { it.patient?.id = patientPseudonym } + this.familyMemberHistories?.forEach { it.patient?.id = patientPseudonym } + this.histologyReports?.forEach { + it.patient.id = patientPseudonym + it.results.tumorMorphology?.patient?.id = patientPseudonym + it.results.tumorCellContent?.patient?.id = patientPseudonym + } + this.ngsReports?.forEach { + it.patient?.id = patientPseudonym + it.results?.simpleVariants?.forEach { it.patient?.id = patientPseudonym } + it.results?.copyNumberVariants?.forEach { it.patient?.id = patientPseudonym } + it.results?.dnaFusions?.forEach { it.patient?.id = patientPseudonym } + it.results?.rnaFusions?.forEach { it.patient?.id = patientPseudonym } + it.results?.tumorCellContent?.patient?.id = patientPseudonym + it.results?.brcaness?.patient?.id = patientPseudonym + it.results?.tmb?.patient?.id = patientPseudonym + it.results?.hrdScore?.patient?.id = patientPseudonym + } + this.ihcReports?.forEach { + it.patient?.id = patientPseudonym + it.results?.msiMmr?.forEach { it.patient?.id = patientPseudonym } + it.results?.proteinExpression?.forEach { it.patient?.id = patientPseudonym } + } + this.responses?.forEach { it.patient?.id = patientPseudonym } + this.specimens?.forEach { it.patient?.id = patientPseudonym } + this.priorDiagnosticReports?.forEach { it.patient?.id = patientPseudonym } + this.performanceStatus?.forEach { it.patient?.id = patientPseudonym } + this.systemicTherapies?.forEach { it.history?.forEach { it.patient?.id = patientPseudonym } } + this.followUps?.forEach { it.patient?.id = patientPseudonym } + + this.msiFindings?.forEach { it -> it.patient.id = patientPseudonym } + + this.metadata?.researchConsents?.forEach { it -> + val entry = it ?: return@forEach + if (entry.contains("patient")) { + // here we expect only a patient reference any other data like display + // need to be removed, since may contain unsecure data + entry.remove("patient") + entry["patient"] = mapOf("reference" to "Patient/$patientPseudonym") } + } } /** * Creates new hash of content IDs with given prefix except for patient IDs * - * @since 0.11.0 - * * @param pseudonymizeService The pseudonymizeService to be used * @return The MTB file containing rehashed content IDs + * @since 0.11.0 */ infix fun Mtb.anonymizeContentWith(pseudonymizeService: PseudonymizeService) { - val prefix = pseudonymizeService.prefix() - - fun anonymize(id: String): String { - val hash = DigestUtils.sha256Hex("$prefix-$id").substring(0, 41).lowercase() - return "$prefix$hash" - } - - this.episodesOfCare?.forEach { - it?.apply { id = id?.let(::anonymize) } - it.diagnoses?.forEach { it -> - it?.id = it.id?.let(::anonymize) - } - } - - this.carePlans?.onEach { carePlan -> - carePlan?.apply { - this.id = id?.let { anonymize(it) } - - this.geneticCounselingRecommendation?.apply { - this.id = this.id?.let(::anonymize) - } - this.rebiopsyRequests?.forEach { it -> - it.id = it.id?.let(::anonymize) - it.tumorEntity?.id = it.tumorEntity?.id?.let(::anonymize) - } - this.histologyReevaluationRequests?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) - } - - this.medicationRecommendations?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.supportingVariants?.forEach { it -> - it.variant?.id = it.variant?.id?.let(::anonymize) - } - it.reason?.id = it.reason?.id?.let(::anonymize) - } - this.reason?.id = this.reason?.id?.let(::anonymize) - this.studyEnrollmentRecommendations?.forEach { it -> - it?.reason?.id = it.reason?.id?.let(::anonymize) - } - this.procedureRecommendations?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.supportingVariants?.forEach { it -> - it.variant?.id = it.variant?.id?.let(::anonymize) - } - - it.reason?.id = it.reason?.id?.let(::anonymize) - - } - this.studyEnrollmentRecommendations?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.supportingVariants.forEach { it -> - it.variant?.id = it?.variant?.id?.let(::anonymize) - } - } - } - } - - - this.responses?.forEach { it -> - - it?.id = it.id?.let(::anonymize) - it?.therapy?.id = it.therapy?.id?.let(::anonymize) - - } - - this.diagnoses?.forEach { it -> - + val prefix = pseudonymizeService.prefix() + + fun anonymize(id: String): String { + val hash = DigestUtils.sha256Hex("$prefix-$id").substring(0, 41).lowercase() + return "$prefix$hash" + } + + this.episodesOfCare?.forEach { + it?.apply { id = id?.let(::anonymize) } + it.diagnoses?.forEach { it -> it?.id = it.id?.let(::anonymize) } + } + + this.carePlans?.onEach { carePlan -> + carePlan?.apply { + this.id = id?.let { anonymize(it) } + + this.geneticCounselingRecommendation?.apply { this.id = this.id?.let(::anonymize) } + this.rebiopsyRequests?.forEach { it -> + it.id = it.id?.let(::anonymize) + it.tumorEntity?.id = it.tumorEntity?.id?.let(::anonymize) + } + this.histologyReevaluationRequests?.forEach { it -> it.id = it?.id?.let(::anonymize) - it.histology?.forEach { it -> it.id = it?.id?.let(::anonymize) } - } - - this.ngsReports?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.results?.tumorCellContent?.id = it.results.tumorCellContent?.id?.let(::anonymize) - it.results?.tumorCellContent?.specimen?.id = - it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) - it.results?.rnaFusions?.forEach { it -> - it?.id = it.id?.let(::anonymize) - } - it.results?.simpleVariants?.forEach { it -> - it?.id = it.id?.let(::anonymize) - it?.transcriptId?.value = it.transcriptId?.value?.let(::anonymize) - } - it.results?.tmb?.id = it.results?.tmb?.id?.let(::anonymize) - it.results?.tmb?.specimen?.id = it.results?.tmb?.specimen?.id?.let(::anonymize) - - it.results?.brcaness?.id = it.results?.brcaness?.id?.let(::anonymize) - it.results?.brcaness?.specimen?.id = it.results?.brcaness?.specimen?.id?.let(::anonymize) - it.results?.copyNumberVariants?.forEach { it -> it?.id = it.id?.let(::anonymize) } - it.results?.hrdScore?.id = it.results?.hrdScore?.id?.let(::anonymize) - it.results?.hrdScore?.specimen?.id = it.results?.hrdScore?.specimen?.id?.let(::anonymize) - it.results?.rnaSeqs?.forEach { it -> it?.id = it.id?.let(::anonymize) } - it.results?.dnaFusions?.forEach { it -> it?.id = it.id?.let(::anonymize) } - it.specimen?.id = it?.specimen?.id?.let(::anonymize) - - } - - this.histologyReports?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.results?.tumorCellContent?.id = it.results?.tumorCellContent?.id?.let(::anonymize) - it.results?.tumorCellContent?.specimen?.id = - it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) - - it.results?.tumorMorphology?.id = it.results?.tumorMorphology?.id?.let(::anonymize) - it.results?.tumorMorphology?.specimen?.id = - it.results?.tumorMorphology?.specimen?.id?.let(::anonymize) it.specimen?.id = it.specimen?.id?.let(::anonymize) + } - } - this.claimResponses?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.claim?.id = it.claim?.id?.let(::anonymize) - } - this.claims?.forEach { it -> - - it.id = it?.id?.let(::anonymize) - it.recommendation?.id = it.recommendation?.id?.let(::anonymize) - - } - this.familyMemberHistories?.forEach { it -> it.id = it?.id?.let(::anonymize) } - this.guidelineProcedures?.forEach { it -> + this.medicationRecommendations?.forEach { it -> it.id = it?.id?.let(::anonymize) + it.supportingVariants?.forEach { it -> it.variant?.id = it.variant?.id?.let(::anonymize) } it.reason?.id = it.reason?.id?.let(::anonymize) - it.basedOn?.id = it.basedOn?.id?.let(::anonymize) - - } - - this.guidelineTherapies?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.reason?.id = it.reason?.id?.let(::anonymize) - it.basedOn?.id = it.basedOn?.id?.let(::anonymize) - } - this.ihcReports?.forEach { it -> - it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) - it.results?.proteinExpression?.forEach { it -> it?.id = it.id.let(::anonymize) } - } - - this.msiFindings?.forEach { it -> - + } + this.reason?.id = this.reason?.id?.let(::anonymize) + this.studyEnrollmentRecommendations?.forEach { it -> + it?.reason?.id = it.reason?.id?.let(::anonymize) + } + this.procedureRecommendations?.forEach { it -> it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) - } - - this.performanceStatus?.forEach { it -> it.id = it?.id?.let(::anonymize) } - - this.priorDiagnosticReports?.forEach { it -> + it.supportingVariants?.forEach { it -> it.variant?.id = it.variant?.id?.let(::anonymize) } + it.reason?.id = it.reason?.id?.let(::anonymize) + } + this.studyEnrollmentRecommendations?.forEach { it -> it.id = it?.id?.let(::anonymize) - it.specimen?.id = it.specimen?.id?.let(::anonymize) + it.supportingVariants.forEach { it -> it.variant?.id = it?.variant?.id?.let(::anonymize) } + } } - - this.specimens?.forEach { it -> - - it.id = it?.id?.let(::anonymize) - it.diagnosis?.id = it.diagnosis?.id?.let(::anonymize) - + } + + this.responses?.forEach { it -> + it?.id = it.id?.let(::anonymize) + it?.therapy?.id = it.therapy?.id?.let(::anonymize) + } + + this.diagnoses?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.histology?.forEach { it -> it.id = it?.id?.let(::anonymize) } + } + + this.ngsReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.results?.tumorCellContent?.id = it.results.tumorCellContent?.id?.let(::anonymize) + it.results?.tumorCellContent?.specimen?.id = + it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) + it.results?.rnaFusions?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.results?.simpleVariants?.forEach { it -> + it?.id = it.id?.let(::anonymize) + it?.transcriptId?.value = it.transcriptId?.value?.let(::anonymize) } - - this.systemicTherapies?.forEach { it -> - - it.history?.forEach { it -> - - it.id = it?.id?.let(::anonymize) - it.reason?.id = it.reason?.id?.let(::anonymize) - it.basedOn?.id = it.basedOn?.id?.let(::anonymize) - } - + it.results?.tmb?.id = it.results?.tmb?.id?.let(::anonymize) + it.results?.tmb?.specimen?.id = it.results?.tmb?.specimen?.id?.let(::anonymize) + + it.results?.brcaness?.id = it.results?.brcaness?.id?.let(::anonymize) + it.results?.brcaness?.specimen?.id = it.results?.brcaness?.specimen?.id?.let(::anonymize) + it.results?.copyNumberVariants?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.results?.hrdScore?.id = it.results?.hrdScore?.id?.let(::anonymize) + it.results?.hrdScore?.specimen?.id = it.results?.hrdScore?.specimen?.id?.let(::anonymize) + it.results?.rnaSeqs?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.results?.dnaFusions?.forEach { it -> it?.id = it.id?.let(::anonymize) } + it.specimen?.id = it?.specimen?.id?.let(::anonymize) + } + + this.histologyReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.results?.tumorCellContent?.id = it.results?.tumorCellContent?.id?.let(::anonymize) + it.results?.tumorCellContent?.specimen?.id = + it.results?.tumorCellContent?.specimen?.id?.let(::anonymize) + + it.results?.tumorMorphology?.id = it.results?.tumorMorphology?.id?.let(::anonymize) + it.results?.tumorMorphology?.specimen?.id = + it.results?.tumorMorphology?.specimen?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + } + this.claimResponses?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.claim?.id = it.claim?.id?.let(::anonymize) + } + this.claims?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.recommendation?.id = it.recommendation?.id?.let(::anonymize) + } + this.familyMemberHistories?.forEach { it -> it.id = it?.id?.let(::anonymize) } + this.guidelineProcedures?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.reason?.id = it.reason?.id?.let(::anonymize) + it.basedOn?.id = it.basedOn?.id?.let(::anonymize) + } + + this.guidelineTherapies?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.reason?.id = it.reason?.id?.let(::anonymize) + it.basedOn?.id = it.basedOn?.id?.let(::anonymize) + } + this.ihcReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + it.results?.proteinExpression?.forEach { it -> it?.id = it.id.let(::anonymize) } + } + + this.msiFindings?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + } + + this.performanceStatus?.forEach { it -> it.id = it?.id?.let(::anonymize) } + + this.priorDiagnosticReports?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.specimen?.id = it.specimen?.id?.let(::anonymize) + } + + this.specimens?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.diagnosis?.id = it.diagnosis?.id?.let(::anonymize) + } + + this.systemicTherapies?.forEach { it -> + it.history?.forEach { it -> + it.id = it?.id?.let(::anonymize) + it.reason?.id = it.reason?.id?.let(::anonymize) + it.basedOn?.id = it.basedOn?.id?.let(::anonymize) } + } } fun Mtb.ensureMetaDataIsInitialized() { - // init metadata if necessary - if (this.metadata == null) { - val mvhMetadata = MvhMetadata.builder().build() - this.metadata = mvhMetadata - } - if (this.metadata.researchConsents == null) { - this.metadata.researchConsents = mutableListOf() - } - if (this.metadata.modelProjectConsent == null) { - this.metadata.modelProjectConsent = ModelProjectConsent() - this.metadata.modelProjectConsent.provisions = mutableListOf() - } else if (this.metadata.modelProjectConsent.provisions != null) { - // make sure list can be changed - this.metadata.modelProjectConsent.provisions = - this.metadata.modelProjectConsent.provisions.toMutableList() - } + // init metadata if necessary + if (this.metadata == null) { + val mvhMetadata = MvhMetadata.builder().build() + this.metadata = mvhMetadata + } + if (this.metadata.researchConsents == null) { + this.metadata.researchConsents = mutableListOf() + } + if (this.metadata.modelProjectConsent == null) { + this.metadata.modelProjectConsent = ModelProjectConsent() + this.metadata.modelProjectConsent.provisions = mutableListOf() + } else if (this.metadata.modelProjectConsent.provisions != null) { + // make sure list can be changed + this.metadata.modelProjectConsent.provisions = + this.metadata.modelProjectConsent.provisions.toMutableList() + } } infix fun Mtb.addGenomDeTan(pseudonymizeService: PseudonymizeService) { - this.metadata?.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id)) + this.metadata?.transferTan = pseudonymizeService.genomDeTan(PatientId(this.patient.id)) } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt index 44b04e8..fdaa7d2 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/TokenService.kt @@ -20,6 +20,8 @@ package dev.dnpm.etl.processor.security import jakarta.annotation.PostConstruct +import java.time.Instant +import java.util.* import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table import org.springframework.data.repository.CrudRepository @@ -27,57 +29,50 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.security.core.userdetails.User import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.provisioning.InMemoryUserDetailsManager -import java.time.Instant -import java.util.* class TokenService( private val userDetailsManager: InMemoryUserDetailsManager, private val passwordEncoder: PasswordEncoder, - private val tokenRepository: TokenRepository + private val tokenRepository: TokenRepository, ) { - @PostConstruct - fun setup() { - tokenRepository.findAll().forEach { - userDetailsManager.createUser( - User.withUsername(it.username) - .password(it.password) - .roles("MTBFILE") - .build() - ) - } + @PostConstruct + fun setup() { + tokenRepository.findAll().forEach { + userDetailsManager.createUser( + User.withUsername(it.username).password(it.password).roles("MTBFILE").build() + ) } + } - fun addToken(name: String): Result<String> { - val username = name.lowercase().replace("""[^a-z0-9]""".toRegex(), "") - if (userDetailsManager.userExists(username)) { - return Result.failure(RuntimeException("Cannot use token name")) - } + fun addToken(name: String): Result<String> { + val username = name.lowercase().replace("""[^a-z0-9]""".toRegex(), "") + if (userDetailsManager.userExists(username)) { + return Result.failure(RuntimeException("Cannot use token name")) + } - val password = Base64.getEncoder().encodeToString(UUID.randomUUID().toString().encodeToByteArray()) - val encodedPassword = passwordEncoder.encode(password).toString() + val password = + Base64.getEncoder().encodeToString(UUID.randomUUID().toString().encodeToByteArray()) + val encodedPassword = passwordEncoder.encode(password).toString() - userDetailsManager.createUser( - User.withUsername(username) - .password(encodedPassword) - .roles("MTBFILE") - .build() - ) + userDetailsManager.createUser( + User.withUsername(username).password(encodedPassword).roles("MTBFILE").build() + ) - tokenRepository.save(Token(name = name, username = username, password = encodedPassword)) + tokenRepository.save(Token(name = name, username = username, password = encodedPassword)) - return Result.success("$username:$password") - } + return Result.success("$username:$password") + } - fun deleteToken(id: Long) { - val token = tokenRepository.findByIdOrNull(id) ?: return - userDetailsManager.deleteUser(token.username) - tokenRepository.delete(token) - } + fun deleteToken(id: Long) { + val token = tokenRepository.findByIdOrNull(id) ?: return + userDetailsManager.deleteUser(token.username) + tokenRepository.delete(token) + } - fun findAll(): List<Token> { - return tokenRepository.findAll().toList() - } + fun findAll(): List<Token> { + return tokenRepository.findAll().toList() + } } @Table("token") @@ -86,7 +81,7 @@ data class Token( val name: String, val username: String, val password: String, - val createdAt: Instant = Instant.now() + val createdAt: Instant = Instant.now(), ) -interface TokenRepository : CrudRepository<Token, Long>
\ No newline at end of file +interface TokenRepository : CrudRepository<Token, Long> diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt index a1d45c8..bfe966a 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRole.kt @@ -20,26 +20,21 @@ package dev.dnpm.etl.processor.security +import java.util.* import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table import org.springframework.data.repository.CrudRepository -import java.util.* @Table("user_role") -data class UserRole( - @Id val id: Long? = null, - val username: String, - var role: Role = Role.GUEST -) +data class UserRole(@Id val id: Long? = null, val username: String, var role: Role = Role.GUEST) enum class Role(val value: String) { - GUEST("guest"), - USER("user"), - ADMIN("admin") + GUEST("guest"), + USER("user"), + ADMIN("admin"), } interface UserRoleRepository : CrudRepository<UserRole, Long> { - fun findByUsername(username: String): Optional<UserRole> - -}
\ No newline at end of file + fun findByUsername(username: String): Optional<UserRole> +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt index 174f8a9..bf46b84 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/security/UserRoleService.kt @@ -25,9 +25,12 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser class UserRoleService( private val userRoleRepository: UserRoleRepository, - private val sessionRegistry: SessionRegistry + private val sessionRegistry: SessionRegistry, ) { - fun updateUserRole(id: Long, role: Role) { + fun updateUserRole( + id: Long, + role: Role, + ) { val userRole = userRoleRepository.findByIdOrNull(id) ?: return userRole.role = role userRoleRepository.save(userRole) @@ -40,19 +43,13 @@ class UserRoleService( expireSessionFor(userRole.username) } - fun findAll(): List<UserRole> { - return userRoleRepository.findAll().toList() - } + fun findAll(): List<UserRole> = userRoleRepository.findAll().toList() private fun expireSessionFor(username: String) { sessionRegistry.allPrincipals .filterIsInstance<OidcUser>() .filter { it.preferredUsername == username } - .flatMap { - sessionRegistry.getAllSessions(it, true) - } - .onEach { - it.expireNow() - } + .flatMap { sessionRegistry.getAllSessions(it, true) } + .onEach { it.expireNow() } } -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt index b420d1f..8437962 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ConsentProcessor.kt @@ -11,6 +11,10 @@ import dev.dnpm.etl.processor.consent.IConsentService import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.pseudonym.ensureMetaDataIsInitialized import dev.pcvolkmer.mv64e.mtb.* +import java.io.IOException +import java.time.Clock +import java.time.Instant +import java.util.* import org.apache.commons.lang3.NotImplementedException import org.hl7.fhir.r4.model.* import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent @@ -19,10 +23,6 @@ import org.hl7.fhir.r4.model.Consent.ProvisionComponent import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.stereotype.Service -import java.io.IOException -import java.time.Clock -import java.time.Instant -import java.util.* @Service class ConsentProcessor( @@ -30,248 +30,271 @@ class ConsentProcessor( private val gIcsConfigProperties: GIcsConfigProperties, private val objectMapper: ObjectMapper, private val fhirContext: FhirContext, - private val consentService: IConsentService + private val consentService: IConsentService, ) { - private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor") - - /** - * In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked. - * - * Logic: - * * <c>true</c> IF consent check is disabled. - * * <c>true</c> IF broad consent (BC) has been given. - * * <c>true</c> BC has been asked AND declined but genomDe consent has been consented. - * * ELSE <c>false</c> is returned. - * - * @param mtbFile File v2 (will be enriched with consent data) - * @return true if consent is given - * - */ - fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { - if (consentService is MtbFileConsentService) { - // consent check is disabled - return true - } - - mtbFile.ensureMetaDataIsInitialized() - - val personIdentifierValue = mtbFile.patient.id - val requestDate = Date.from(Instant.now(Clock.systemUTC())) + private var logger: Logger = LoggerFactory.getLogger("ConsentProcessor") + + /** + * In case an instance of {@link ICheckConsent} is active, consent will be embedded and checked. + * + * Logic: + * * <c>true</c> IF consent check is disabled. + * * <c>true</c> IF broad consent (BC) has been given. + * * <c>true</c> BC has been asked AND declined but genomDe consent has been consented. + * * ELSE <c>false</c> is returned. + * + * @param mtbFile File v2 (will be enriched with consent data) + * @return true if consent is given + */ + fun consentGatedCheckAndTryEmbedding(mtbFile: Mtb): Boolean { + if (consentService is MtbFileConsentService) { + // consent check is disabled + return true + } - // 1. Broad consent Entry exists? - // 1.1. -> yes and research consent is given -> send mtb file - // 1.2. -> no -> return status error - consent has not been asked - // 2. -> Broad consent found but rejected -> is GenomDe consent provision 'sequencing' given? - // 2.1 -> yes -> send mtb file - // 2.2 -> no -> warn/info no consent given + mtbFile.ensureMetaDataIsInitialized() - /* - * broad consent - */ - val broadConsent = consentService.getConsent( - personIdentifierValue, requestDate, ConsentDomain.BROAD_CONSENT - ) - val broadConsentHasBeenAsked = broadConsent.entry.isNotEmpty() + val personIdentifierValue = mtbFile.patient.id + val requestDate = Date.from(Instant.now(Clock.systemUTC())) - // fast exit - if patient has not been asked, we can skip and exit - if (!broadConsentHasBeenAsked) return false + // 1. Broad consent Entry exists? + // 1.1. -> yes and research consent is given -> send mtb file + // 1.2. -> no -> return status error - consent has not been asked + // 2. -> Broad consent found but rejected -> is GenomDe consent provision 'sequencing' given? + // 2.1 -> yes -> send mtb file + // 2.2 -> no -> warn/info no consent given - val genomeDeConsent = consentService.getConsent( - personIdentifierValue, requestDate, ConsentDomain.MODELLVORHABEN_64E + /* + * broad consent + */ + val broadConsent = + consentService.getConsent(personIdentifierValue, requestDate, ConsentDomain.BROAD_CONSENT) + val broadConsentHasBeenAsked = broadConsent.entry.isNotEmpty() + + // fast exit - if patient has not been asked, we can skip and exit + if (!broadConsentHasBeenAsked) return false + + val genomeDeConsent = + consentService.getConsent( + personIdentifierValue, + requestDate, + ConsentDomain.MODELLVORHABEN_64E, ) - addGenomeDbProvisions(mtbFile, genomeDeConsent) + addGenomeDbProvisions(mtbFile, genomeDeConsent) - if (genomeDeConsent.entry.isNotEmpty()) setGenomDeSubmissionType(mtbFile) + if (genomeDeConsent.entry.isNotEmpty()) setGenomDeSubmissionType(mtbFile) - embedBroadConsentResources(mtbFile, broadConsent) + embedBroadConsentResources(mtbFile, broadConsent) - val broadConsentStatus = getProvisionTypeByPolicyCode( - broadConsent, requestDate, ConsentDomain.BROAD_CONSENT - ) - - val genomDeSequencingStatus = getProvisionTypeByPolicyCode( - genomeDeConsent, requestDate, ConsentDomain.MODELLVORHABEN_64E - ) + val broadConsentStatus = + getProvisionTypeByPolicyCode(broadConsent, requestDate, ConsentDomain.BROAD_CONSENT) - if (Consent.ConsentProvisionType.NULL == broadConsentStatus) { - // bc not asked - return false - } - if (Consent.ConsentProvisionType.PERMIT == broadConsentStatus || Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus) return true + val genomDeSequencingStatus = + getProvisionTypeByPolicyCode(genomeDeConsent, requestDate, ConsentDomain.MODELLVORHABEN_64E) - return false + if (Consent.ConsentProvisionType.NULL == broadConsentStatus) { + // bc not asked + return false } - - fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) { - for (entry in broadConsent.entry) { - val resource = entry.resource - if (resource is Consent) { - // since jackson convertValue does not work here, - // we need another step to back to string, before we convert to object map - val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource) - try { - val mapOfJson: HashMap<String?, Any?>? = - objectMapper.readValue<HashMap<String?, Any?>?>( - asJsonString, object : TypeReference<HashMap<String?, Any?>?>() {}) - mtbFile.metadata.researchConsents.add(mapOfJson) - } catch (e: JsonProcessingException) { - throw RuntimeException(e) - } - } + if ( + Consent.ConsentProvisionType.PERMIT == broadConsentStatus || + Consent.ConsentProvisionType.PERMIT == genomDeSequencingStatus + ) + return true + + return false + } + + fun embedBroadConsentResources(mtbFile: Mtb, broadConsent: Bundle) { + for (entry in broadConsent.entry) { + val resource = entry.resource + if (resource is Consent) { + // since jackson convertValue does not work here, + // we need another step to back to string, before we convert to object map + val asJsonString = fhirContext.newJsonParser().encodeResourceToString(resource) + try { + val mapOfJson: HashMap<String?, Any?>? = + objectMapper.readValue<HashMap<String?, Any?>?>( + asJsonString, + object : TypeReference<HashMap<String?, Any?>?>() {}, + ) + mtbFile.metadata.researchConsents.add(mapOfJson) + } catch (e: JsonProcessingException) { + throw RuntimeException(e) } + } } - - fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) { - for (entry in consentGnomeDe.entry) { - val resource = entry.resource - if (resource !is Consent) { - continue - } - - // We expect only one provision in collection, therefore get first or none - val provisions = resource.provision.provision - if (provisions.isEmpty()) { - continue - } - - val provisionComponent: ProvisionComponent = provisions.first() - val provisionCode = getProvisionCode(provisionComponent) - if (provisionCode != null) { - try { - val modelProjectConsentPurpose = - ModelProjectConsentPurpose.forValue(provisionCode) - - if (ModelProjectConsentPurpose.SEQUENCING == modelProjectConsentPurpose) { - // CONVENTION: wrapping date is date of SEQUENCING consent - mtbFile.metadata.modelProjectConsent.date = resource.dateTime - } - - val provision = Provision.builder() - .type(ConsentProvision.valueOf(provisionComponent.type.name)) - .date(provisionComponent.period.start) - .purpose(modelProjectConsentPurpose).build() - - mtbFile.metadata.modelProjectConsent.provisions.add(provision) - } catch (ioe: IOException) { - logger.error( - "Provision code '$provisionCode' is unknown and cannot be mapped.", - ioe.toString() - ) - } - } - - if (mtbFile.metadata.modelProjectConsent.provisions.isNotEmpty()) { - mtbFile.metadata.modelProjectConsent.version = - gIcsConfigProperties.genomeDeConsentVersion - } + } + + fun addGenomeDbProvisions(mtbFile: Mtb, consentGnomeDe: Bundle) { + for (entry in consentGnomeDe.entry) { + val resource = entry.resource + if (resource !is Consent) { + continue + } + + // We expect only one provision in collection, therefore get first or none + val provisions = resource.provision.provision + if (provisions.isEmpty()) { + continue + } + + val provisionComponent: ProvisionComponent = provisions.first() + val provisionCode = getProvisionCode(provisionComponent) + if (provisionCode != null) { + try { + val modelProjectConsentPurpose = ModelProjectConsentPurpose.forValue(provisionCode) + + if (ModelProjectConsentPurpose.SEQUENCING == modelProjectConsentPurpose) { + // CONVENTION: wrapping date is date of SEQUENCING consent + mtbFile.metadata.modelProjectConsent.date = resource.dateTime + } + + val provision = + Provision.builder() + .type(ConsentProvision.valueOf(provisionComponent.type.name)) + .date(provisionComponent.period.start) + .purpose(modelProjectConsentPurpose) + .build() + + mtbFile.metadata.modelProjectConsent.provisions.add(provision) + } catch (ioe: IOException) { + logger.error( + "Provision code '$provisionCode' is unknown and cannot be mapped.", + ioe.toString(), + ) } - } + } - private fun getProvisionCode(provisionComponent: ProvisionComponent): String? { - var provisionCode: String? = null - if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) { - val codableConcept: CodeableConcept = provisionComponent.code.first() - if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) { - provisionCode = codableConcept.coding.first().code - } - } - return provisionCode + if (mtbFile.metadata.modelProjectConsent.provisions.isNotEmpty()) { + mtbFile.metadata.modelProjectConsent.version = gIcsConfigProperties.genomeDeConsentVersion + } } - - private fun setGenomDeSubmissionType(mtbFile: Mtb) { - if (appConfigProperties.genomDeTestSubmission) { - mtbFile.metadata.type = MvhSubmissionType.TEST - logger.info("genomeDe submission mit TEST") - } else { - mtbFile.metadata.type = when (mtbFile.metadata.type) { - null -> MvhSubmissionType.INITIAL - else -> mtbFile.metadata.type - } - } + } + + private fun getProvisionCode(provisionComponent: ProvisionComponent): String? { + var provisionCode: String? = null + if (provisionComponent.code != null && provisionComponent.code.isNotEmpty()) { + val codableConcept: CodeableConcept = provisionComponent.code.first() + if (codableConcept.coding != null && codableConcept.coding.isNotEmpty()) { + provisionCode = codableConcept.coding.first().code + } } - - /** - * @param consentBundle consent resource - * @param requestDate date which must be within validation period of provision - * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found. - */ - fun getProvisionTypeByPolicyCode( - consentBundle: Bundle, requestDate: Date?, consentDomain: ConsentDomain - ): Consent.ConsentProvisionType { - val code: String? - val system: String? - if (ConsentDomain.BROAD_CONSENT == consentDomain) { - code = gIcsConfigProperties.broadConsentPolicyCode - system = gIcsConfigProperties.broadConsentPolicySystem - } else if (ConsentDomain.MODELLVORHABEN_64E == consentDomain) { - code = gIcsConfigProperties.genomeDePolicyCode - system = gIcsConfigProperties.genomeDePolicySystem - } else { - throw NotImplementedException("unknown consent domain " + consentDomain.name) - } - - val provisionTypeByPolicyCode = getProvisionTypeByPolicyCode( - consentBundle, code, system, requestDate - ) - return provisionTypeByPolicyCode + return provisionCode + } + + private fun setGenomDeSubmissionType(mtbFile: Mtb) { + if (appConfigProperties.genomDeTestSubmission) { + mtbFile.metadata.type = MvhSubmissionType.TEST + logger.info("genomeDe submission mit TEST") + } else { + mtbFile.metadata.type = + when (mtbFile.metadata.type) { + null -> MvhSubmissionType.INITIAL + else -> mtbFile.metadata.type + } + } + } + + /** + * @param consentBundle consent resource + * @param requestDate date which must be within validation period of provision + * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if + * none is found. + */ + fun getProvisionTypeByPolicyCode( + consentBundle: Bundle, + requestDate: Date?, + consentDomain: ConsentDomain, + ): Consent.ConsentProvisionType { + val code: String? + val system: String? + if (ConsentDomain.BROAD_CONSENT == consentDomain) { + code = gIcsConfigProperties.broadConsentPolicyCode + system = gIcsConfigProperties.broadConsentPolicySystem + } else if (ConsentDomain.MODELLVORHABEN_64E == consentDomain) { + code = gIcsConfigProperties.genomeDePolicyCode + system = gIcsConfigProperties.genomeDePolicySystem + } else { + throw NotImplementedException("unknown consent domain " + consentDomain.name) } - /** - * @param consentBundle consent resource - * @param targetCode policyRule and provision code value - * @param targetSystem policyRule and provision system value - * @param requestDate date which must be within validation period of provision - * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if none is found. - */ - fun getProvisionTypeByPolicyCode( - consentBundle: Bundle, targetCode: String?, targetSystem: String?, requestDate: Date? - ): Consent.ConsentProvisionType { - val entriesOfInterest = consentBundle.entry.filter { entry -> - val isConsentResource = - entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent - val consentIsActive = (entry.resource as Consent).status == ConsentState.ACTIVE - - val provisions = (entry.resource as Consent).provision.provision - - val isValidCoding = checkProvisionExist( - targetCode, targetSystem, provisions - ) - - isConsentResource && consentIsActive && isValidCoding && isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period) - }.map { entry: BundleEntryComponent -> - val consent = (entry.getResource() as Consent) - consent.provision.provision.filter { subProvision -> - isRequestDateInRange(requestDate, subProvision.period) - // search coding entries of current provision for code and system - subProvision.code.map { c -> c.coding }.flatten().any { code -> - targetCode.equals(code.code) && targetSystem.equals(code.system) - } - }.map { subProvision -> - subProvision + val provisionTypeByPolicyCode = + getProvisionTypeByPolicyCode(consentBundle, code, system, requestDate) + return provisionTypeByPolicyCode + } + + /** + * @param consentBundle consent resource + * @param targetCode policyRule and provision code value + * @param targetSystem policyRule and provision system value + * @param requestDate date which must be within validation period of provision + * @return type of provision, will be [org.hl7.fhir.r4.model.Consent.ConsentProvisionType.NULL] if + * none is found. + */ + fun getProvisionTypeByPolicyCode( + consentBundle: Bundle, + targetCode: String?, + targetSystem: String?, + requestDate: Date?, + ): Consent.ConsentProvisionType { + val entriesOfInterest = + consentBundle.entry + .filter { entry -> + val isConsentResource = + entry.resource.isResource && entry.resource.resourceType == ResourceType.Consent + val consentIsActive = (entry.resource as Consent).status == ConsentState.ACTIVE + + val provisions = (entry.resource as Consent).provision.provision + + val isValidCoding = checkProvisionExist(targetCode, targetSystem, provisions) + + isConsentResource && + consentIsActive && + isValidCoding && + isRequestDateInRange(requestDate, (entry.resource as Consent).provision.period) } - }.flatten() + .map { entry: BundleEntryComponent -> + val consent = (entry.getResource() as Consent) + consent.provision.provision + .filter { subProvision -> + isRequestDateInRange(requestDate, subProvision.period) + // search coding entries of current provision for code and system + subProvision.code + .map { c -> c.coding } + .flatten() + .any { code -> + targetCode.equals(code.code) && targetSystem.equals(code.system) + } + } + .map { subProvision -> subProvision } + } + .flatten() - if (entriesOfInterest.isNotEmpty()) { - return entriesOfInterest.first().type - } - return Consent.ConsentProvisionType.NULL + if (entriesOfInterest.isNotEmpty()) { + return entriesOfInterest.first().type } - - fun checkProvisionExist( - researchAllowedPolicyOid: String?, - researchAllowedPolicySystem: String?, - provisions: Collection<ProvisionComponent> - ): Boolean { - return provisions.any { provision -> - provision.code.any { codeableConcept -> codeableConcept.coding.any { it.system == researchAllowedPolicySystem && it.code == researchAllowedPolicyOid } } + return Consent.ConsentProvisionType.NULL + } + + fun checkProvisionExist( + researchAllowedPolicyOid: String?, + researchAllowedPolicySystem: String?, + provisions: Collection<ProvisionComponent>, + ): Boolean { + return provisions.any { provision -> + provision.code.any { codeableConcept -> + codeableConcept.coding.any { + it.system == researchAllowedPolicySystem && it.code == researchAllowedPolicyOid } + } } + } - fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean { - val isRequestDateAfterOrEqualStart = provPeriod.start.compareTo(requestDate) - val isRequestDateBeforeOrEqualEnd = provPeriod.end.compareTo(requestDate) - return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0 - } - + fun isRequestDateInRange(requestDate: Date?, provPeriod: Period): Boolean { + val isRequestDateAfterOrEqualStart = provPeriod.start.compareTo(requestDate) + val isRequestDateBeforeOrEqualEnd = provPeriod.end.compareTo(requestDate) + return isRequestDateAfterOrEqualStart <= 0 && isRequestDateBeforeOrEqualEnd >= 0 + } } 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 07d8a8d..4721f75 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestProcessor.kt @@ -38,14 +38,14 @@ import dev.dnpm.etl.processor.pseudonym.pseudonymizeWith import dev.pcvolkmer.mv64e.mtb.ConsentProvision import dev.pcvolkmer.mv64e.mtb.ModelProjectConsentPurpose import dev.pcvolkmer.mv64e.mtb.Mtb +import java.time.Instant +import java.util.* import org.apache.commons.codec.binary.Base32 import org.apache.commons.codec.digest.DigestUtils import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service -import java.time.Instant -import java.util.* @Service class RequestProcessor( @@ -56,163 +56,178 @@ class RequestProcessor( private val objectMapper: ObjectMapper, private val applicationEventPublisher: ApplicationEventPublisher, private val appConfigProperties: AppConfigProperties, - private val consentProcessor: ConsentProcessor? + private val consentProcessor: ConsentProcessor?, ) { - private var logger: Logger = LoggerFactory.getLogger("RequestProcessor") - - fun processMtbFile(mtbFile: Mtb) { - processMtbFile(mtbFile, randomRequestId()) + private var logger: Logger = LoggerFactory.getLogger("RequestProcessor") + + fun processMtbFile(mtbFile: Mtb) { + processMtbFile(mtbFile, randomRequestId()) + } + + fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { + val pid = PatientId(extractPatientIdentifier(mtbFile)) + + val isConsentOk = + consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || + consentProcessor == null + if (isConsentOk) { + if (isGenomDeConsented(mtbFile)) { + mtbFile addGenomDeTan pseudonymizeService + } + mtbFile pseudonymizeWith pseudonymizeService + mtbFile anonymizeContentWith pseudonymizeService + val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) + saveAndSend(request, pid) + } else { + logger.warn("consent check failed file will not be processed further!") + applicationEventPublisher.publishEvent( + ResponseEvent(requestId, Instant.now(), RequestStatus.NO_CONSENT) + ) } + } - - fun processMtbFile(mtbFile: Mtb, requestId: RequestId) { - val pid = PatientId(extractPatientIdentifier(mtbFile)) - - val isConsentOk = - consentProcessor != null && consentProcessor.consentGatedCheckAndTryEmbedding(mtbFile) || consentProcessor == null - if (isConsentOk) { - if (isGenomDeConsented(mtbFile)) { - mtbFile addGenomDeTan pseudonymizeService - } - mtbFile pseudonymizeWith pseudonymizeService - mtbFile anonymizeContentWith pseudonymizeService - val request = DnpmV2MtbFileRequest(requestId, transformationService.transform(mtbFile)) - saveAndSend(request, pid) - } else { - logger.warn("consent check failed file will not be processed further!") - applicationEventPublisher.publishEvent( - ResponseEvent( - requestId, Instant.now(), RequestStatus.NO_CONSENT - ) - ) - } - } - - private fun isGenomDeConsented(mtbFile: Mtb): Boolean { - val isModelProjectConsented = mtbFile.metadata?.modelProjectConsent?.provisions?.any { p -> - p.purpose == ModelProjectConsentPurpose.SEQUENCING && p.type == ConsentProvision.PERMIT + private fun isGenomDeConsented(mtbFile: Mtb): Boolean { + val isModelProjectConsented = + mtbFile.metadata?.modelProjectConsent?.provisions?.any { p -> + p.purpose == ModelProjectConsentPurpose.SEQUENCING && p.type == ConsentProvision.PERMIT } == true - return isModelProjectConsented - } - - private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) { - requestService.save( - Request( - request.requestId, - request.patientPseudonym(), - pid, - fingerprint(request), - RequestType.MTB_FILE, - RequestStatus.UNKNOWN - ) + return isModelProjectConsented + } + + private fun <T> saveAndSend(request: MtbFileRequest<T>, pid: PatientId) { + requestService.save( + Request( + request.requestId, + request.patientPseudonym(), + pid, + fingerprint(request), + RequestType.MTB_FILE, + RequestStatus.UNKNOWN, ) + ) - if (appConfigProperties.duplicationDetection && isDuplication(request)) { - applicationEventPublisher.publishEvent( - ResponseEvent( - request.requestId, Instant.now(), RequestStatus.DUPLICATION - ) - ) - return - } - - val responseStatus = sender.send(request) - - applicationEventPublisher.publishEvent( - ResponseEvent( - request.requestId, - Instant.now(), - responseStatus.status, - when (responseStatus.status) { - RequestStatus.ERROR, RequestStatus.WARNING -> Optional.of(responseStatus.body) - else -> Optional.empty() - } - ) - ) + if (appConfigProperties.duplicationDetection && isDuplication(request)) { + applicationEventPublisher.publishEvent( + ResponseEvent(request.requestId, Instant.now(), RequestStatus.DUPLICATION) + ) + return } - private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean { - val patientPseudonym = when (pseudonymizedMtbFileRequest) { - is DnpmV2MtbFileRequest -> PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id) - } - - val lastMtbFileRequestForPatient = - requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) - val isLastRequestDeletion = - requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) - - return null != lastMtbFileRequestForPatient && !isLastRequestDeletion && lastMtbFileRequestForPatient.fingerprint == fingerprint( - pseudonymizedMtbFileRequest + val responseStatus = sender.send(request) + + applicationEventPublisher.publishEvent( + ResponseEvent( + request.requestId, + Instant.now(), + responseStatus.status, + when (responseStatus.status) { + RequestStatus.ERROR, + RequestStatus.WARNING -> Optional.of(responseStatus.body) + else -> Optional.empty() + }, ) - } - - fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) { - processDeletion(patientId, randomRequestId(), isConsented) - } - - fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) { - try { - val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) - - val requestStatus: RequestStatus = when (isConsented) { - TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, TtpConsentStatus.BROAD_CONSENT_MISSING, TtpConsentStatus.BROAD_CONSENT_REJECTED -> RequestStatus.NO_CONSENT - TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR - TtpConsentStatus.BROAD_CONSENT_GIVEN, TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN - TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, TtpConsentStatus.GENOM_DE_CONSENT_MISSING, TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED -> { - throw RuntimeException("processDelete should never deal with '" + isConsented.name + "' consent status. This is a bug and need to be fixed!") - } - } - - requestService.save( - Request( - requestId, - patientPseudonym, - patientId, - fingerprint(patientPseudonym.value), - RequestType.DELETE, - requestStatus - ) - ) - - val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym)) - - applicationEventPublisher.publishEvent( - ResponseEvent( - requestId, Instant.now(), responseStatus.status, when (responseStatus.status) { - RequestStatus.WARNING, RequestStatus.ERROR -> Optional.of(responseStatus.body) - else -> Optional.empty() - } - ) - ) - - } catch (_: Exception) { - requestService.save( - Request( - uuid = requestId, - patientPseudonym = emptyPatientPseudonym(), - pid = patientId, - fingerprint = Fingerprint.empty(), - status = RequestStatus.ERROR, - type = RequestType.DELETE, - report = Report("Fehler bei der Pseudonymisierung") - ) - ) + ) + } + + private fun <T> isDuplication(pseudonymizedMtbFileRequest: MtbFileRequest<T>): Boolean { + val patientPseudonym = + when (pseudonymizedMtbFileRequest) { + is DnpmV2MtbFileRequest -> + PatientPseudonym(pseudonymizedMtbFileRequest.content.patient.id) } - } - private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint { - return when (request) { - is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content)) - } + val lastMtbFileRequestForPatient = + requestService.lastMtbFileRequestForPatientPseudonym(patientPseudonym) + val isLastRequestDeletion = + requestService.isLastRequestWithKnownStatusDeletion(patientPseudonym) + + return null != lastMtbFileRequestForPatient && + !isLastRequestDeletion && + lastMtbFileRequestForPatient.fingerprint == fingerprint(pseudonymizedMtbFileRequest) + } + + fun processDeletion(patientId: PatientId, isConsented: TtpConsentStatus) { + processDeletion(patientId, randomRequestId(), isConsented) + } + + fun processDeletion(patientId: PatientId, requestId: RequestId, isConsented: TtpConsentStatus) { + try { + val patientPseudonym = pseudonymizeService.patientPseudonym(patientId) + + val requestStatus: RequestStatus = + when (isConsented) { + TtpConsentStatus.BROAD_CONSENT_MISSING_OR_REJECTED, + TtpConsentStatus.BROAD_CONSENT_MISSING, + TtpConsentStatus.BROAD_CONSENT_REJECTED -> RequestStatus.NO_CONSENT + TtpConsentStatus.FAILED_TO_ASK -> RequestStatus.ERROR + TtpConsentStatus.BROAD_CONSENT_GIVEN, + TtpConsentStatus.UNKNOWN_CHECK_FILE -> RequestStatus.UNKNOWN + TtpConsentStatus.GENOM_DE_CONSENT_SEQUENCING_PERMIT, + TtpConsentStatus.GENOM_DE_CONSENT_MISSING, + TtpConsentStatus.GENOM_DE_SEQUENCING_REJECTED -> { + throw RuntimeException( + "processDelete should never deal with '" + + isConsented.name + + "' consent status. This is a bug and need to be fixed!" + ) + } + } + + requestService.save( + Request( + requestId, + patientPseudonym, + patientId, + fingerprint(patientPseudonym.value), + RequestType.DELETE, + requestStatus, + ) + ) + + val responseStatus = sender.send(DeleteRequest(requestId, patientPseudonym)) + + applicationEventPublisher.publishEvent( + ResponseEvent( + requestId, + Instant.now(), + responseStatus.status, + when (responseStatus.status) { + RequestStatus.WARNING, + RequestStatus.ERROR -> Optional.of(responseStatus.body) + else -> Optional.empty() + }, + ) + ) + } catch (_: Exception) { + requestService.save( + Request( + uuid = requestId, + patientPseudonym = emptyPatientPseudonym(), + pid = patientId, + fingerprint = Fingerprint.empty(), + status = RequestStatus.ERROR, + type = RequestType.DELETE, + report = Report("Fehler bei der Pseudonymisierung"), + ) + ) } + } - private fun fingerprint(s: String): Fingerprint { - return Fingerprint( - Base32().encodeAsString(DigestUtils.sha256(s)).replace("=", "").lowercase() - ) + private fun <T> fingerprint(request: MtbFileRequest<T>): Fingerprint { + return when (request) { + is DnpmV2MtbFileRequest -> fingerprint(objectMapper.writeValueAsString(request.content)) } - + } + + private fun fingerprint(s: String): Fingerprint { + return Fingerprint( + Base32() + .encodeAsString(DigestUtils.sha256(s)) + .replace("=", "") + .lowercase() + ) + } } private fun extractPatientIdentifier(mtbFile: Mtb): String = mtbFile.patient.id diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt index 757b353..e7cb95f 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/RequestService.kt @@ -22,55 +22,63 @@ package dev.dnpm.etl.processor.services import dev.dnpm.etl.processor.PatientPseudonym import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.* +import java.util.* import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service -import java.util.* @Service -class RequestService( - private val requestRepository: RequestRepository -) { +class RequestService(private val requestRepository: RequestRepository) { - fun save(request: Request) = requestRepository.save(request) + fun save(request: Request) = requestRepository.save(request) - fun findAll(): Iterable<Request> = requestRepository.findAll() + fun findAll(): Iterable<Request> = requestRepository.findAll() - fun findAll(pageable: Pageable): Page<Request> = requestRepository.findAll(pageable) + fun findAll(pageable: Pageable): Page<Request> = requestRepository.findAll(pageable) - fun findByUuid(uuid: RequestId): Optional<Request> = - requestRepository.findByUuidEquals(uuid) + fun findByUuid(uuid: RequestId): Optional<Request> = requestRepository.findByUuidEquals(uuid) - fun findRequestByPatientId(patientPseudonym: PatientPseudonym, pageable: Pageable): Page<Request> = requestRepository.findRequestByPatientPseudonym(patientPseudonym, pageable) + fun findRequestByPatientId( + patientPseudonym: PatientPseudonym, + pageable: Pageable, + ): Page<Request> = requestRepository.findRequestByPatientPseudonym(patientPseudonym, pageable) - fun allRequestsByPatientPseudonym(patientPseudonym: PatientPseudonym) = requestRepository - .findAllByPatientPseudonymOrderByProcessedAtDesc(patientPseudonym) + fun allRequestsByPatientPseudonym(patientPseudonym: PatientPseudonym) = + requestRepository.findAllByPatientPseudonymOrderByProcessedAtDesc(patientPseudonym) - fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: PatientPseudonym) = - Companion.lastMtbFileRequestForPatientPseudonym(allRequestsByPatientPseudonym(patientPseudonym)) + fun lastMtbFileRequestForPatientPseudonym(patientPseudonym: PatientPseudonym) = + Companion.lastMtbFileRequestForPatientPseudonym( + allRequestsByPatientPseudonym(patientPseudonym) + ) - fun isLastRequestWithKnownStatusDeletion(patientPseudonym: PatientPseudonym) = - Companion.isLastRequestWithKnownStatusDeletion(allRequestsByPatientPseudonym(patientPseudonym)) + fun isLastRequestWithKnownStatusDeletion(patientPseudonym: PatientPseudonym) = + Companion.isLastRequestWithKnownStatusDeletion( + allRequestsByPatientPseudonym(patientPseudonym) + ) - fun countStates(): Iterable<CountedState> = requestRepository.countStates() + fun countStates(): Iterable<CountedState> = requestRepository.countStates() - fun countDeleteStates(): Iterable<CountedState> = requestRepository.countDeleteStates() + fun countDeleteStates(): Iterable<CountedState> = requestRepository.countDeleteStates() - fun findPatientUniqueStates(): List<CountedState> = requestRepository.findPatientUniqueStates() + fun findPatientUniqueStates(): List<CountedState> = requestRepository.findPatientUniqueStates() - fun findPatientUniqueDeleteStates(): List<CountedState> = requestRepository.findPatientUniqueDeleteStates() + fun findPatientUniqueDeleteStates(): List<CountedState> = + requestRepository.findPatientUniqueDeleteStates() - companion object { + companion object { - fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = allRequests + fun lastMtbFileRequestForPatientPseudonym(allRequests: List<Request>) = + allRequests .filter { it.type == RequestType.MTB_FILE } .sortedByDescending { it.processedAt } - .firstOrNull { it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING } + .firstOrNull { + it.status == RequestStatus.SUCCESS || it.status == RequestStatus.WARNING + } - fun isLastRequestWithKnownStatusDeletion(allRequests: List<Request>) = allRequests + fun isLastRequestWithKnownStatusDeletion(allRequests: List<Request>) = + allRequests .filter { it.status != RequestStatus.UNKNOWN } - .maxByOrNull { it.processedAt }?.type == RequestType.DELETE - - } - -}
\ No newline at end of file + .maxByOrNull { it.processedAt } + ?.type == RequestType.DELETE + } +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt index fb82647..190cefe 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/ResponseProcessor.kt @@ -22,79 +22,76 @@ package dev.dnpm.etl.processor.services import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.Report import dev.dnpm.etl.processor.monitoring.RequestStatus +import java.time.Instant +import java.util.* import org.slf4j.LoggerFactory import org.springframework.context.event.EventListener import org.springframework.stereotype.Service import reactor.core.publisher.Sinks -import java.time.Instant -import java.util.* @Service class ResponseProcessor( private val requestService: RequestService, - private val statisticsUpdateProducer: Sinks.Many<Any> + private val statisticsUpdateProducer: Sinks.Many<Any>, ) { - private val logger = LoggerFactory.getLogger(ResponseProcessor::class.java) + private val logger = LoggerFactory.getLogger(ResponseProcessor::class.java) - @EventListener(classes = [ResponseEvent::class]) - fun handleResponseEvent(event: ResponseEvent) { - requestService.findByUuid(event.requestUuid).ifPresentOrElse({ - it.processedAt = event.timestamp - it.status = event.status + @EventListener(classes = [ResponseEvent::class]) + fun handleResponseEvent(event: ResponseEvent) { + requestService + .findByUuid(event.requestUuid) + .ifPresentOrElse( + { + it.processedAt = event.timestamp + it.status = event.status - when (event.status) { + when (event.status) { RequestStatus.SUCCESS -> { - it.report = Report( - "Keine Probleme erkannt", - ) + it.report = + Report( + "Keine Probleme erkannt", + ) } RequestStatus.WARNING -> { - it.report = Report( - "Warnungen über mangelhafte Daten", - event.body.orElse("") - ) + it.report = Report("Warnungen über mangelhafte Daten", event.body.orElse("")) } RequestStatus.ERROR -> { - it.report = Report( - "Fehler bei der Datenübertragung oder Inhalt nicht verarbeitbar", - event.body.orElse("") - ) + it.report = + Report( + "Fehler bei der Datenübertragung oder Inhalt nicht verarbeitbar", + event.body.orElse(""), + ) } RequestStatus.DUPLICATION -> { - it.report = Report( - "Duplikat erkannt" - ) + it.report = Report("Duplikat erkannt") } RequestStatus.NO_CONSENT -> { - it.report = Report( - "Einwilligung Status fehlt, widerrufen oder ungeklärt." - ) + it.report = Report("Einwilligung Status fehlt, widerrufen oder ungeklärt.") } else -> { - logger.error("Cannot process response: Unknown response!") - return@ifPresentOrElse + logger.error("Cannot process response: Unknown response!") + return@ifPresentOrElse } - } - - requestService.save(it) + } - statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST) - }, { - logger.error("Response for unknown request '${event.requestUuid}'!") - }) - } + requestService.save(it) + statisticsUpdateProducer.emitNext("", Sinks.EmitFailureHandler.FAIL_FAST) + }, + { logger.error("Response for unknown request '${event.requestUuid}'!") }, + ) + } } data class ResponseEvent( val requestUuid: RequestId, val timestamp: Instant, val status: RequestStatus, - val body: Optional<String> = Optional.empty() -)
\ No newline at end of file + val body: Optional<String> = Optional.empty(), +) diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt index 8f1081e..df8ac3d 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/TransformationService.kt @@ -24,8 +24,10 @@ import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.PathNotFoundException import dev.pcvolkmer.mv64e.mtb.Mtb -class TransformationService(private val objectMapper: ObjectMapper, private val transformations: List<Transformation>) { - +class TransformationService( + private val objectMapper: ObjectMapper, + private val transformations: List<Transformation>, +) { fun transform(mtbFile: Mtb): Mtb { val json = transform(objectMapper.writeValueAsString(mtbFile)) return objectMapper.readValue(json, Mtb::class.java) @@ -41,12 +43,24 @@ class TransformationService(private val objectMapper: ObjectMapper, private val val before = transformation.path.substringBeforeLast(".") val last = transformation.path.substringAfterLast(".") - val existingValue = if (transformation.existingValue is Number) transformation.existingValue else transformation.existingValue.toString() - val newValue = if (transformation.newValue is Number) transformation.newValue else transformation.newValue.toString() - - jsonPath.set("$.$before.[?]$last", newValue, { - it.item(HashMap::class.java)[last] == existingValue - }) + val existingValue = + if (transformation.existingValue is Number) { + transformation.existingValue + } else { + transformation.existingValue.toString() + } + val newValue = + if (transformation.newValue is Number) { + transformation.newValue + } else { + transformation.newValue.toString() + } + + jsonPath.set( + "$.$before.[?]$last", + newValue, + { it.item(HashMap::class.java)[last] == existingValue }, + ) } catch (e: PathNotFoundException) { // Ignore } @@ -57,35 +71,30 @@ class TransformationService(private val objectMapper: ObjectMapper, private val return json } - fun getTransformations(): List<Transformation> { - return this.transformations - } - + fun getTransformations(): List<Transformation> = this.transformations } -class Transformation private constructor(val path: String) { - - lateinit var existingValue: Any - private set - lateinit var newValue: Any - private set - - infix fun from(value: Any): Transformation { - this.existingValue = value - return this - } +class Transformation + private constructor( + val path: String, + ) { + lateinit var existingValue: Any + private set - infix fun to(value: Any): Transformation { - this.newValue = value - return this - } + lateinit var newValue: Any + private set - companion object { + infix fun from(value: Any): Transformation { + this.existingValue = value + return this + } - fun of(path: String): Transformation { - return Transformation(path) + infix fun to(value: Any): Transformation { + this.newValue = value + return this } + companion object { + fun of(path: String): Transformation = Transformation(path) + } } - -} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt index fd6a9b4..e70f1e7 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessor.kt @@ -26,56 +26,64 @@ import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.output.asRequestStatus import dev.dnpm.etl.processor.services.ResponseEvent +import java.time.Instant +import java.util.* import org.apache.kafka.clients.consumer.ConsumerRecord import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.kafka.listener.MessageListener -import java.time.Instant -import java.util.* class KafkaResponseProcessor( private val eventPublisher: ApplicationEventPublisher, - private val objectMapper: ObjectMapper + private val objectMapper: ObjectMapper, ) : MessageListener<String, String> { - private val logger = LoggerFactory.getLogger(KafkaResponseProcessor::class.java) + private val logger = LoggerFactory.getLogger(KafkaResponseProcessor::class.java) - override fun onMessage(data: ConsumerRecord<String, String>) { - try { - Optional.of(objectMapper.readValue(data.value(), ResponseBody::class.java)) + override fun onMessage(data: ConsumerRecord<String, String>) { + try { + Optional.of(objectMapper.readValue(data.value(), ResponseBody::class.java)) } catch (e: Exception) { - logger.error("Cannot process Kafka response", e) - Optional.empty() - }.ifPresentOrElse({ responseBody -> - val event = ResponseEvent( - RequestId(responseBody.requestId), - Instant.ofEpochMilli(data.timestamp()), - responseBody.statusCode.asRequestStatus(), - when (responseBody.statusCode.asRequestStatus()) { - RequestStatus.SUCCESS -> { - Optional.empty() - } - - RequestStatus.WARNING, RequestStatus.ERROR -> { - Optional.of(objectMapper.writeValueAsString(responseBody.statusBody)) - } + logger.error("Cannot process Kafka response", e) + Optional.empty() + } + .ifPresentOrElse( + { responseBody -> + val event = + ResponseEvent( + RequestId(responseBody.requestId), + Instant.ofEpochMilli(data.timestamp()), + responseBody.statusCode.asRequestStatus(), + when (responseBody.statusCode.asRequestStatus()) { + RequestStatus.SUCCESS -> { + Optional.empty() + } - else -> { - logger.error("Kafka response: Unknown response code '{}'!", responseBody.statusCode) - Optional.empty() - } - } - ) - eventPublisher.publishEvent(event) - }, { - logger.error("No requestId in Kafka response") - }) - } + RequestStatus.WARNING, + RequestStatus.ERROR -> { + Optional.of(objectMapper.writeValueAsString(responseBody.statusBody)) + } - data class ResponseBody( - @param:JsonProperty("request_id") @param:JsonAlias("requestId") val requestId: String, - @param:JsonProperty("status_code") @param:JsonAlias("statusCode") val statusCode: Int, - @param:JsonProperty("status_body") @param:JsonAlias("statusBody") val statusBody: Map<String, Any> - ) + else -> { + logger.error( + "Kafka response: Unknown response code '{}'!", + responseBody.statusCode, + ) + Optional.empty() + } + }, + ) + eventPublisher.publishEvent(event) + }, + { logger.error("No requestId in Kafka response") }, + ) + } -}
\ No newline at end of file + data class ResponseBody( + @param:JsonProperty("request_id") @param:JsonAlias("requestId") val requestId: String, + @param:JsonProperty("status_code") @param:JsonAlias("statusCode") val statusCode: Int, + @param:JsonProperty("status_body") + @param:JsonAlias("statusBody") + val statusBody: Map<String, Any>, + ) +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/types.kt b/src/main/kotlin/dev/dnpm/etl/processor/types.kt index 90fa7cb..c7aa110 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/types.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/types.kt @@ -19,33 +19,30 @@ package dev.dnpm.etl.processor -import org.springframework.http.MediaType import java.util.* +import org.springframework.http.MediaType class Fingerprint(val value: String) { - override fun hashCode() = value.hashCode() + override fun hashCode() = value.hashCode() - override fun equals(other: Any?) = other is Fingerprint && other.value == value + override fun equals(other: Any?) = other is Fingerprint && other.value == value - companion object { - fun empty() = Fingerprint("") - } + companion object { + fun empty() = Fingerprint("") + } } @JvmInline value class RequestId(val value: String) { - fun isBlank() = value.isBlank() - + fun isBlank() = value.isBlank() } fun randomRequestId() = RequestId(UUID.randomUUID().toString()) -@JvmInline -value class PatientId(val value: String) +@JvmInline value class PatientId(val value: String) -@JvmInline -value class PatientPseudonym(val value: String) +@JvmInline value class PatientPseudonym(val value: String) fun emptyPatientPseudonym() = PatientPseudonym("") @@ -55,9 +52,9 @@ fun emptyPatientPseudonym() = PatientPseudonym("") * @since 0.11.0 */ object CustomMediaType { - val APPLICATION_VND_DNPM_V2_MTB_JSON = MediaType("application", "vnd.dnpm.v2.mtb+json") - const val APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE = "application/vnd.dnpm.v2.mtb+json" + val APPLICATION_VND_DNPM_V2_MTB_JSON = MediaType("application", "vnd.dnpm.v2.mtb+json") + const val APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE = "application/vnd.dnpm.v2.mtb+json" - val APPLICATION_VND_DNPM_V2_RD_JSON = MediaType("application", "vnd.dnpm.v2.rd+json") - const val APPLICATION_VND_DNPM_V2_RD_JSON_VALUE = "application/vnd.dnpm.v2.rd+json" + val APPLICATION_VND_DNPM_V2_RD_JSON = MediaType("application", "vnd.dnpm.v2.rd+json") + const val APPLICATION_VND_DNPM_V2_RD_JSON_VALUE = "application/vnd.dnpm.v2.rd+json" } diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt index bdca57e..0dec30e 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ApplicationControllerAdvice.kt @@ -27,11 +27,7 @@ import org.springframework.web.bind.annotation.ResponseStatus @ControllerAdvice class ApplicationControllerAdvice { - @ExceptionHandler(NotFoundException::class) @ResponseStatus(HttpStatus.NOT_FOUND) - fun handleNotFoundException(e: NotFoundException): String { - return "errors/404" - } - -}
\ No newline at end of file + fun handleNotFoundException(e: NotFoundException): String = "errors/404" +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt index 44571d4..b77bdf9 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/ConfigController.kt @@ -23,11 +23,11 @@ import dev.dnpm.etl.processor.monitoring.* import dev.dnpm.etl.processor.output.MtbFileSender import dev.dnpm.etl.processor.pseudonym.Generator import dev.dnpm.etl.processor.security.Role -import dev.dnpm.etl.processor.security.UserRole import dev.dnpm.etl.processor.security.Token import dev.dnpm.etl.processor.security.TokenService -import dev.dnpm.etl.processor.services.TransformationService +import dev.dnpm.etl.processor.security.UserRole import dev.dnpm.etl.processor.security.UserRoleService +import dev.dnpm.etl.processor.services.TransformationService import org.springframework.beans.factory.annotation.Qualifier import org.springframework.http.MediaType import org.springframework.http.codec.ServerSentEvent @@ -47,175 +47,193 @@ class ConfigController( private val mtbFileSender: MtbFileSender, private val connectionCheckServices: List<ConnectionCheckService>, private val tokenService: TokenService?, - private val userRoleService: UserRoleService? + private val userRoleService: UserRoleService?, ) { - @GetMapping - fun index(model: Model): String { - val outputConnectionAvailable = - connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().firstOrNull()?.connectionAvailable() - - val gPasConnectionAvailable = - connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable() - - val gIcsConnectionAvailable = - connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable() - - model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName) - model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) - model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) - model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) - model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) - model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) - model.addAttribute("tokensEnabled", tokenService != null) - if (tokenService != null) { - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokens", emptyList<Token>()) - } - model.addAttribute("transformations", transformationService.getTransformations()) - if (userRoleService != null) { - model.addAttribute("userRolesEnabled", true) - model.addAttribute("userRoles", userRoleService.findAll()) - } else { - model.addAttribute("userRolesEnabled", false) - model.addAttribute("userRoles", emptyList<UserRole>()) - } - return "configs" + @GetMapping + fun index(model: Model): String { + val outputConnectionAvailable = + connectionCheckServices + .filterIsInstance<OutputConnectionCheckService>() + .firstOrNull() + ?.connectionAvailable() + + val gPasConnectionAvailable = + connectionCheckServices + .filterIsInstance<GPasConnectionCheckService>() + .firstOrNull() + ?.connectionAvailable() + + val gIcsConnectionAvailable = + connectionCheckServices + .filterIsInstance<GIcsConnectionCheckService>() + .firstOrNull() + ?.connectionAvailable() + + model.addAttribute("pseudonymGenerator", pseudonymGenerator.javaClass.simpleName) + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) + model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) + model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) + model.addAttribute("tokensEnabled", tokenService != null) + if (tokenService != null) { + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", emptyList<Token>()) } - - @GetMapping(params = ["outputConnectionAvailable"]) - fun outputConnectionAvailable(model: Model): String { - val outputConnectionAvailable = - connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().first().connectionAvailable() - - model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) - model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) - model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) - if (tokenService != null) { - model.addAttribute("tokensEnabled", true) - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokens", listOf<Token>()) - } - - return "configs/outputConnectionAvailable" + model.addAttribute("transformations", transformationService.getTransformations()) + if (userRoleService != null) { + model.addAttribute("userRolesEnabled", true) + model.addAttribute("userRoles", userRoleService.findAll()) + } else { + model.addAttribute("userRolesEnabled", false) + model.addAttribute("userRoles", emptyList<UserRole>()) } - - @GetMapping(params = ["gPasConnectionAvailable"]) - fun gPasConnectionAvailable(model: Model): String { - val gPasConnectionAvailable = - connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable() - - model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) - model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) - model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) - if (tokenService != null) { - model.addAttribute("tokensEnabled", true) - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokens", listOf<Token>()) - } - - return "configs/gPasConnectionAvailable" + return "configs" + } + + @GetMapping(params = ["outputConnectionAvailable"]) + fun outputConnectionAvailable(model: Model): String { + val outputConnectionAvailable = + connectionCheckServices + .filterIsInstance<OutputConnectionCheckService>() + .first() + .connectionAvailable() + + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("outputConnectionAvailable", outputConnectionAvailable) + if (tokenService != null) { + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", listOf<Token>()) } - @GetMapping(params = ["gIcsConnectionAvailable"]) - fun gIcsConnectionAvailable(model: Model): String { - val gIcsConnectionAvailable = - connectionCheckServices.filterIsInstance<GIcsConnectionCheckService>().firstOrNull()?.connectionAvailable() - - model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) - model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) - model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) - if (tokenService != null) { - model.addAttribute("tokensEnabled", true) - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokens", listOf<Token>()) - } - - return "configs/gIcsConnectionAvailable" + return "configs/outputConnectionAvailable" + } + + @GetMapping(params = ["gPasConnectionAvailable"]) + fun gPasConnectionAvailable(model: Model): String { + val gPasConnectionAvailable = + connectionCheckServices + .filterIsInstance<GPasConnectionCheckService>() + .firstOrNull() + ?.connectionAvailable() + + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("gPasConnectionAvailable", gPasConnectionAvailable) + if (tokenService != null) { + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", listOf<Token>()) } - @PostMapping(path = ["tokens"]) - fun addToken(@ModelAttribute("name") name: String, model: Model): String { - if (tokenService == null) { - model.addAttribute("tokensEnabled", false) - model.addAttribute("success", false) - } else { - model.addAttribute("tokensEnabled", true) - val result = tokenService.addToken(name) - result.onSuccess { - model.addAttribute("newTokenValue", it) - model.addAttribute("success", true) - } - result.onFailure { - model.addAttribute("success", false) - } - model.addAttribute("tokens", tokenService.findAll()) - } - - return "configs/tokens" + return "configs/gPasConnectionAvailable" + } + + @GetMapping(params = ["gIcsConnectionAvailable"]) + fun gIcsConnectionAvailable(model: Model): String { + val gIcsConnectionAvailable = + connectionCheckServices + .filterIsInstance<GIcsConnectionCheckService>() + .firstOrNull() + ?.connectionAvailable() + + model.addAttribute("mtbFileSender", mtbFileSender.javaClass.simpleName) + model.addAttribute("mtbFileEndpoint", mtbFileSender.endpoint()) + model.addAttribute("gIcsConnectionAvailable", gIcsConnectionAvailable) + if (tokenService != null) { + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokens", listOf<Token>()) } - @DeleteMapping(path = ["tokens/{id}"]) - fun deleteToken(@PathVariable id: Long, model: Model): String { - if (tokenService != null) { - tokenService.deleteToken(id) - - model.addAttribute("tokensEnabled", true) - model.addAttribute("tokens", tokenService.findAll()) - } else { - model.addAttribute("tokensEnabled", false) - model.addAttribute("tokens", listOf<Token>()) - } - return "configs/tokens" + return "configs/gIcsConnectionAvailable" + } + + @PostMapping(path = ["tokens"]) + fun addToken(@ModelAttribute("name") name: String, model: Model): String { + if (tokenService == null) { + model.addAttribute("tokensEnabled", false) + model.addAttribute("success", false) + } else { + model.addAttribute("tokensEnabled", true) + val result = tokenService.addToken(name) + result.onSuccess { + model.addAttribute("newTokenValue", it) + model.addAttribute("success", true) + } + result.onFailure { model.addAttribute("success", false) } + model.addAttribute("tokens", tokenService.findAll()) } - @DeleteMapping(path = ["userroles/{id}"]) - fun deleteUserRole(@PathVariable id: Long, model: Model): String { - if (userRoleService != null) { - userRoleService.deleteUserRole(id) - - model.addAttribute("userRolesEnabled", true) - model.addAttribute("userRoles", userRoleService.findAll()) - } else { - model.addAttribute("userRolesEnabled", false) - model.addAttribute("userRoles", emptyList<UserRole>()) - } - return "configs/userroles" - } + return "configs/tokens" + } - @PutMapping(path = ["userroles/{id}"]) - fun updateUserRole(@PathVariable id: Long, @ModelAttribute("role") role: Role, model: Model): String { - if (userRoleService != null) { - userRoleService.updateUserRole(id, role) - - model.addAttribute("userRolesEnabled", true) - model.addAttribute("userRoles", userRoleService.findAll()) - } else { - model.addAttribute("userRolesEnabled", false) - model.addAttribute("userRoles", emptyList<UserRole>()) - } - return "configs/userroles" - } + @DeleteMapping(path = ["tokens/{id}"]) + fun deleteToken(@PathVariable id: Long, model: Model): String { + if (tokenService != null) { + tokenService.deleteToken(id) - @GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) - @ResponseBody - fun events(): Flux<ServerSentEvent<Any>> { - return connectionCheckUpdateProducer.asFlux().map { - val event = when (it) { - is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check" - is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check" - is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check" - is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check" - } - - ServerSentEvent.builder<Any>() - .event(event).id("none").data(it) - .build() - } + model.addAttribute("tokensEnabled", true) + model.addAttribute("tokens", tokenService.findAll()) + } else { + model.addAttribute("tokensEnabled", false) + model.addAttribute("tokens", listOf<Token>()) } - -}
\ No newline at end of file + return "configs/tokens" + } + + @DeleteMapping(path = ["userroles/{id}"]) + fun deleteUserRole(@PathVariable id: Long, model: Model): String { + if (userRoleService != null) { + userRoleService.deleteUserRole(id) + + model.addAttribute("userRolesEnabled", true) + model.addAttribute("userRoles", userRoleService.findAll()) + } else { + model.addAttribute("userRolesEnabled", false) + model.addAttribute("userRoles", emptyList<UserRole>()) + } + return "configs/userroles" + } + + @PutMapping(path = ["userroles/{id}"]) + fun updateUserRole( + @PathVariable id: Long, + @ModelAttribute("role") role: Role, + model: Model, + ): String { + if (userRoleService != null) { + userRoleService.updateUserRole(id, role) + + model.addAttribute("userRolesEnabled", true) + model.addAttribute("userRoles", userRoleService.findAll()) + } else { + model.addAttribute("userRolesEnabled", false) + model.addAttribute("userRoles", emptyList<UserRole>()) + } + return "configs/userroles" + } + + @GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) + @ResponseBody + fun events(): Flux<ServerSentEvent<Any>> { + return connectionCheckUpdateProducer.asFlux().map { + val event = + when (it) { + is ConnectionCheckResult.KafkaConnectionCheckResult -> "output-connection-check" + is ConnectionCheckResult.RestConnectionCheckResult -> "output-connection-check" + is ConnectionCheckResult.GPasConnectionCheckResult -> "gpas-connection-check" + is ConnectionCheckResult.GIcsConnectionCheckResult -> "gics-connection-check" + } + + ServerSentEvent.builder<Any>().event(event).id("none").data(it).build() + } + } +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt index 54920b1..082cd20 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt @@ -37,13 +37,13 @@ import org.springframework.web.bind.annotation.RequestMapping @RequestMapping(path = ["/"]) class HomeController( private val requestService: RequestService, - private val reportService: ReportService + private val reportService: ReportService, ) { - @GetMapping fun index( - @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable, - model: Model + @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) + pageable: Pageable, + model: Model, ): String { val requests = requestService.findAll(pageable) model.addAttribute("requests", requests) @@ -54,8 +54,9 @@ class HomeController( @GetMapping(path = ["patient/{patientPseudonym}"]) fun byPatient( @PathVariable patientPseudonym: PatientPseudonym, - @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) pageable: Pageable, - model: Model + @PageableDefault(page = 0, size = 20, sort = ["processedAt"], direction = Sort.Direction.DESC) + pageable: Pageable, + model: Model, ): String { val requests = requestService.findRequestByPatientId(patientPseudonym, pageable) model.addAttribute("patientPseudonym", patientPseudonym.value) @@ -65,12 +66,14 @@ class HomeController( } @GetMapping(path = ["/report/{id}"]) - fun report(@PathVariable id: RequestId, model: Model): String { + fun report( + @PathVariable id: RequestId, + model: Model, + ): String { val request = requestService.findByUuid(id).orElse(null) ?: throw NotFoundException() model.addAttribute("request", request) model.addAttribute("issues", reportService.deserialize(request.report?.dataQualityReport)) return "report" } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt index 20837bb..7821bf9 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/LoginController.kt @@ -28,20 +28,21 @@ import org.springframework.web.bind.annotation.GetMapping @Controller class LoginController( private val securityConfigProperties: SecurityConfigProperties?, - private val oAuth2ClientProperties: OAuth2ClientProperties? + private val oAuth2ClientProperties: OAuth2ClientProperties?, ) { - @GetMapping(path = ["/login"]) fun login(model: Model): String { if (securityConfigProperties?.enableOidc == true) { model.addAttribute( "oidcLogins", - oAuth2ClientProperties?.registration?.map { (key, value) -> Pair(key, value.clientName) }.orEmpty() + oAuth2ClientProperties + ?.registration + ?.map { (key, value) -> Pair(key, value.clientName) } + .orEmpty(), ) } else { model.addAttribute("oidcLogins", emptyList<Pair<String, String>>()) } return "login" } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt index adc1e2b..e48d5df 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsController.kt @@ -28,11 +28,9 @@ import java.time.Instant @Controller @RequestMapping(path = ["/statistics"]) class StatisticsController { - @GetMapping fun index(model: Model): String { model.addAttribute("now", Instant.now()) return "statistics" } - -}
\ No newline at end of file +} diff --git a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt index 8372274..c99a4a3 100644 --- a/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt +++ b/src/main/kotlin/dev/dnpm/etl/processor/web/StatisticsRestController.kt @@ -41,140 +41,168 @@ import java.time.temporal.ChronoUnit class StatisticsRestController( @param:Qualifier("statisticsUpdateProducer") private val statisticsUpdateProducer: Sinks.Many<Any>, - private val requestService: RequestService + private val requestService: RequestService, ) { - @GetMapping(path = ["requeststates"]) - fun requestStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> { - val states = if (delete) { - requestService.countDeleteStates() - } else { - requestService.countStates() - } + fun requestStates( + @RequestParam(required = false, defaultValue = "false") delete: Boolean, + ): List<NameValue> { + val states = + if (delete) { + requestService.countDeleteStates() + } else { + requestService.countStates() + } return states .map { - val color = when (it.status) { - RequestStatus.ERROR -> "red" - RequestStatus.WARNING -> "darkorange" - RequestStatus.SUCCESS -> "green" - else -> "slategray" - } + val color = + when (it.status) { + RequestStatus.ERROR -> "red" + RequestStatus.WARNING -> "darkorange" + RequestStatus.SUCCESS -> "green" + else -> "slategray" + } NameValue(it.status.toString(), it.count, color) - } - .sortedByDescending { it.value } + }.sortedByDescending { it.value } } @GetMapping(path = ["requestslastmonth"]) fun requestsLastMonth( - @RequestParam( - required = false, - defaultValue = "false" - ) delete: Boolean + @RequestParam(required = false, defaultValue = "false") delete: Boolean, ): List<DateNameValues> { - val requestType = if (delete) { - RequestType.DELETE - } else { - RequestType.MTB_FILE - } + val requestType = + if (delete) { + RequestType.DELETE + } else { + RequestType.MTB_FILE + } val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Europe/Berlin")) - val data = requestService.findAll() - .filter { it.type == requestType } - .filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) } - .groupBy { formatter.format(it.processedAt) } - .map { - val requestList = it.value - .groupBy { request -> request.status } - .map { request -> - Pair(request.key, request.value.size) - } - .toMap() - Pair( - it.key.toString(), - DateNameValues( - it.key.toString(), NameValues( - error = requestList[RequestStatus.ERROR] ?: 0, - warning = requestList[RequestStatus.WARNING] ?: 0, - success = requestList[RequestStatus.SUCCESS] ?: 0, - duplication = requestList[RequestStatus.DUPLICATION] ?: 0, - unknown = requestList[RequestStatus.UNKNOWN] ?: 0, - ) + val data = + requestService + .findAll() + .filter { it.type == requestType } + .filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) } + .groupBy { formatter.format(it.processedAt) } + .map { + val requestList = + it.value + .groupBy { request -> request.status } + .map { request -> Pair(request.key, request.value.size) } + .toMap() + Pair( + it.key.toString(), + DateNameValues( + it.key.toString(), + NameValues( + error = requestList[RequestStatus.ERROR] ?: 0, + warning = requestList[RequestStatus.WARNING] ?: 0, + success = requestList[RequestStatus.SUCCESS] ?: 0, + duplication = requestList[RequestStatus.DUPLICATION] ?: 0, + unknown = requestList[RequestStatus.UNKNOWN] ?: 0, + ), + ), ) - ) - }.toMap() + }.toMap() - return (0L..30L).map { Instant.now().minus(it, ChronoUnit.DAYS) } + return (0L..30L) + .map { Instant.now().minus(it, ChronoUnit.DAYS) } .map { formatter.format(it) } - .map { - DateNameValues(it, data[it]?.nameValues ?: NameValues()) - } + .map { DateNameValues(it, data[it]?.nameValues ?: NameValues()) } .sortedBy { it.date } } @GetMapping(path = ["requestpatientstates"]) - fun requestPatientStates(@RequestParam(required = false, defaultValue = "false") delete: Boolean): List<NameValue> { - val states = if (delete) { - requestService.findPatientUniqueDeleteStates() - } else { - requestService.findPatientUniqueStates() - } + fun requestPatientStates( + @RequestParam(required = false, defaultValue = "false") delete: Boolean, + ): List<NameValue> { + val states = + if (delete) { + requestService.findPatientUniqueDeleteStates() + } else { + requestService.findPatientUniqueStates() + } return states.map { - val color = when (it.status) { - RequestStatus.ERROR -> "red" - RequestStatus.WARNING -> "darkorange" - RequestStatus.SUCCESS -> "green" - else -> "slategray" - } + val color = + when (it.status) { + RequestStatus.ERROR -> "red" + RequestStatus.WARNING -> "darkorange" + RequestStatus.SUCCESS -> "green" + else -> "slategray" + } NameValue(it.status.toString(), it.count, color) } } @GetMapping(path = ["events"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) - fun updater(): Flux<ServerSentEvent<Any>> { - return statisticsUpdateProducer.asFlux().flatMap { + fun updater(): Flux<ServerSentEvent<Any>> = + statisticsUpdateProducer.asFlux().flatMap { Flux.fromIterable( listOf( - ServerSentEvent.builder<Any>() - .event("requeststates").id("none").data(this.requestStates(false)) + ServerSentEvent + .builder<Any>() + .event("requeststates") + .id("none") + .data(this.requestStates(false)) .build(), - ServerSentEvent.builder<Any>() - .event("requestslastmonth").id("none").data(this.requestsLastMonth(false)) + ServerSentEvent + .builder<Any>() + .event("requestslastmonth") + .id("none") + .data(this.requestsLastMonth(false)) .build(), - ServerSentEvent.builder<Any>() - .event("requestpatientstates").id("none").data(this.requestPatientStates(false)) + ServerSentEvent + .builder<Any>() + .event("requestpatientstates") + .id("none") + .data(this.requestPatientStates(false)) .build(), - - ServerSentEvent.builder<Any>() - .event("deleterequeststates").id("none").data(this.requestStates(true)) + ServerSentEvent + .builder<Any>() + .event("deleterequeststates") + .id("none") + .data(this.requestStates(true)) .build(), - ServerSentEvent.builder<Any>() - .event("deleterequestslastmonth").id("none").data(this.requestsLastMonth(true)) + ServerSentEvent + .builder<Any>() + .event("deleterequestslastmonth") + .id("none") + .data(this.requestsLastMonth(true)) .build(), - ServerSentEvent.builder<Any>() - .event("deleterequestpatientstates").id("none").data(this.requestPatientStates(true)) + ServerSentEvent + .builder<Any>() + .event("deleterequestpatientstates") + .id("none") + .data(this.requestPatientStates(true)) .build(), - - ServerSentEvent.builder<Any>() - .event("newrequest").id("none").data("newrequest") - .build() - ) + ServerSentEvent + .builder<Any>() + .event("newrequest") + .id("none") + .data("newrequest") + .build(), + ), ) - } - } - } -data class NameValue(val name: String, val value: Int, val color: String) +data class NameValue( + val name: String, + val value: Int, + val color: String, +) -data class DateNameValues(val date: String, val nameValues: NameValues) +data class DateNameValues( + val date: String, + val nameValues: NameValues, +) data class NameValues( val error: Int = 0, val warning: Int = 0, val success: Int = 0, val duplication: Int = 0, - val unknown: Int = 0 -)
\ No newline at end of file + val unknown: Int = 0, +) diff --git a/src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt index 5a86a29..1140425 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/consent/ConsentProcessorTest.kt @@ -5,6 +5,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.services.ConsentProcessor +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Consent @@ -14,45 +15,46 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension -import java.util.* @ExtendWith(MockitoExtension::class) class ConsentProcessorTest { - lateinit var consentProcessor: ConsentProcessor + lateinit var consentProcessor: ConsentProcessor - val objectMapper = ObjectMapper() - val fhirContext = FhirContext.forR4() + val objectMapper = ObjectMapper() + val fhirContext = FhirContext.forR4() - @BeforeEach - fun setup( - @Mock consentService: IConsentService - ) { - val appConfigProperties = AppConfigProperties() - val gIcsConfigProperties = GIcsConfigProperties("http://localhost") + @BeforeEach + fun setup(@Mock consentService: IConsentService) { + val appConfigProperties = AppConfigProperties() + val gIcsConfigProperties = GIcsConfigProperties("http://localhost") - this.consentProcessor = ConsentProcessor( + this.consentProcessor = + ConsentProcessor( appConfigProperties, gIcsConfigProperties, objectMapper, fhirContext, - consentService + consentService, ) - } - - @ParameterizedTest - @CsvSource(value = [ - "permittedConsentBundle.json,permit", - "deniedConsentBundle.json,deny" - ]) - fun checkGetProvisionTypeByPolicyCode(filename: String, expected: String) { - val bundle = fhirContext.newJsonParser().parseResource( - this.javaClass.classLoader.getResourceAsStream(filename) + } + + @ParameterizedTest + @CsvSource(value = ["permittedConsentBundle.json,permit", "deniedConsentBundle.json,deny"]) + fun checkGetProvisionTypeByPolicyCode(filename: String, expected: String) { + val bundle = + fhirContext + .newJsonParser() + .parseResource(this.javaClass.classLoader.getResourceAsStream(filename)) + assertThat(bundle).isInstanceOf(Bundle::class.java) + + val actual = + consentProcessor.getProvisionTypeByPolicyCode( + bundle as Bundle, + Date(), + ConsentDomain.BROAD_CONSENT, ) - assertThat(bundle).isInstanceOf(Bundle::class.java) - - val actual = consentProcessor.getProvisionTypeByPolicyCode(bundle as Bundle, Date(), ConsentDomain.BROAD_CONSENT) - assertThat(actual).isEqualTo(Consent.ConsentProvisionType.valueOf(expected.uppercase())) - } -}
\ No newline at end of file + assertThat(actual).isEqualTo(Consent.ConsentProvisionType.valueOf(expected.uppercase())) + } +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/consent/Dnpm21BasedConsentEvaluatorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/consent/Dnpm21BasedConsentEvaluatorTest.kt index adbec2f..85a8b3e 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/consent/Dnpm21BasedConsentEvaluatorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/consent/Dnpm21BasedConsentEvaluatorTest.kt @@ -21,6 +21,8 @@ package dev.dnpm.etl.processor.consent import dev.dnpm.etl.processor.ArgProvider import dev.pcvolkmer.mv64e.mtb.* +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -32,256 +34,269 @@ 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 { + @Nested + inner class WithGicsConsentEnabled { - lateinit var consentService: GicsConsentService - lateinit var consentEvaluator: ConsentEvaluator + lateinit var consentService: GicsConsentService + lateinit var consentEvaluator: ConsentEvaluator - @BeforeEach - fun setUp( - @Mock consentService: GicsConsentService - ) { - this.consentService = consentService - this.consentEvaluator = ConsentEvaluator(consentService) - } + @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) - } + @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 { + @Nested + inner class WithFileConsentOnly { - lateinit var consentService: MtbFileConsentService - lateinit var consentEvaluator: ConsentEvaluator + lateinit var consentService: MtbFileConsentService + lateinit var consentEvaluator: ConsentEvaluator - @BeforeEach - fun setUp() { - this.consentService = MtbFileConsentService() - this.consentEvaluator = ConsentEvaluator(consentService) - } + @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) - } + @ParameterizedTest + @ArgumentsSource(MtbFileProvider::class) + fun test(mtbFile: Mtb, expectedConsentEvaluation: ConsentEvaluation) { + assertThat(consentEvaluator.check(mtbFile)).isEqualTo(expectedConsentEvaluation) } + } - // Util classes + // 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) - ) - ) { + 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( + 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() + 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() ) .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) - ) - ) { + 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) - } + 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( + 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() + 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() ) .build() - } - } + ) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("TEST_12345678").build()) + .build() + ) + ) + .build() + } } - + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt b/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt index 2dfb1e1..495bf38 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/helpers.kt @@ -19,13 +19,12 @@ package dev.dnpm.etl.processor +import java.util.stream.Stream 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) + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = + Stream.of(*data) } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt index 7f07766..da05fba 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/KafkaInputListenerTest.kt @@ -26,6 +26,7 @@ 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 java.util.* import org.apache.kafka.clients.consumer.ConsumerRecord import org.apache.kafka.common.header.internals.RecordHeader import org.apache.kafka.common.header.internals.RecordHeaders @@ -36,267 +37,241 @@ import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.* -import java.util.* @ExtendWith(MockitoExtension::class) class KafkaInputListenerTest { - private lateinit var requestProcessor: RequestProcessor - private lateinit var consentEvaluator: ConsentEvaluator - private lateinit var objectMapper: ObjectMapper + private lateinit var requestProcessor: RequestProcessor + private lateinit var consentEvaluator: ConsentEvaluator + private lateinit var objectMapper: ObjectMapper - private lateinit var kafkaInputListener: KafkaInputListener + private lateinit var kafkaInputListener: KafkaInputListener - @BeforeEach - fun setup( - @Mock requestProcessor: RequestProcessor, - @Mock consentEvaluator: ConsentEvaluator, - ) { - this.requestProcessor = requestProcessor - this.consentEvaluator = consentEvaluator - this.objectMapper = ObjectMapper() + @BeforeEach + fun setup( + @Mock requestProcessor: RequestProcessor, + @Mock consentEvaluator: ConsentEvaluator, + ) { + this.requestProcessor = requestProcessor + this.consentEvaluator = consentEvaluator + this.objectMapper = ObjectMapper() - this.kafkaInputListener = KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) - } + this.kafkaInputListener = KafkaInputListener(requestProcessor, consentEvaluator, objectMapper) + } - @Test - fun shouldProcessMtbFileRequest() { - whenever(consentEvaluator.check(any())).thenReturn( - ConsentEvaluation( - TtpConsentStatus.BROAD_CONSENT_GIVEN, - true - ) - ) + @Test + fun shouldProcessMtbFileRequest() { + whenever(consentEvaluator.check(any())) + .thenReturn(ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true)) - val mtbFile = Mtb.builder() + val mtbFile = + Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( - MvhMetadata - .builder() + MvhMetadata.builder() .modelProjectConsent( - ModelProjectConsent - .builder() + ModelProjectConsent.builder() .provisions( listOf( - Provision.builder().type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + Provision.builder() + .type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() ) - ).build() + ) + .build() ) .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<Mtb>()) - } + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>()) + } - @Test - fun shouldProcessDeleteRequest() { - whenever(consentEvaluator.check(any())).thenReturn( - ConsentEvaluation( - TtpConsentStatus.BROAD_CONSENT_GIVEN, - false - ) - ) + @Test + fun shouldProcessDeleteRequest() { + whenever(consentEvaluator.check(any())) + .thenReturn(ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, false)) - val mtbFile = Mtb.builder() + val mtbFile = + Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( - MvhMetadata - .builder() + MvhMetadata.builder() .modelProjectConsent( - ModelProjectConsent - .builder() + ModelProjectConsent.builder() .provisions( listOf( - Provision.builder().type(ConsentProvision.DENY) - .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + Provision.builder() + .type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() ) - ).build() + ) + .build() ) .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(), - eq(TtpConsentStatus.UNKNOWN_CHECK_FILE) - ) - } + verify(requestProcessor, times(1)) + .processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)) + } - @Test - fun shouldProcessMtbFileRequestWithExistingRequestId() { - whenever(consentEvaluator.check(any())).thenReturn( - ConsentEvaluation( - TtpConsentStatus.BROAD_CONSENT_GIVEN, - true - ) - ) + @Test + fun shouldProcessMtbFileRequestWithExistingRequestId() { + whenever(consentEvaluator.check(any())) + .thenReturn(ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true)) - val mtbFile = Mtb.builder() + val mtbFile = + Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( - MvhMetadata - .builder() + MvhMetadata.builder() .modelProjectConsent( - ModelProjectConsent - .builder() + ModelProjectConsent.builder() .provisions( listOf( - Provision.builder().type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + Provision.builder() + .type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() ) - ).build() + ) + .build() ) .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)).processMtbFile(any<Mtb>(), anyValueClass()) - } + verify(requestProcessor, times(1)).processMtbFile(any<Mtb>(), anyValueClass()) + } - @Test - fun shouldProcessDeleteRequestWithExistingRequestId() { - whenever(consentEvaluator.check(any())).thenReturn( - ConsentEvaluation( - TtpConsentStatus.BROAD_CONSENT_GIVEN, - false - ) - ) + @Test + fun shouldProcessDeleteRequestWithExistingRequestId() { + whenever(consentEvaluator.check(any())) + .thenReturn(ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, false)) - val mtbFile = Mtb.builder() + val mtbFile = + Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( - MvhMetadata - .builder() + MvhMetadata.builder() .modelProjectConsent( - ModelProjectConsent - .builder() + ModelProjectConsent.builder() .provisions( listOf( - Provision.builder().type(ConsentProvision.DENY) - .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + Provision.builder() + .type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() ) - ).build() + ) + .build() ) .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() - ) - ) - verify(requestProcessor, times(1)).processDeletion( - anyValueClass(), anyValueClass(), eq( - TtpConsentStatus.UNKNOWN_CHECK_FILE - ) + 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(TtpConsentStatus.UNKNOWN_CHECK_FILE)) + } - @Test - fun shouldProcessDnpmV2Request() { - whenever(consentEvaluator.check(any())).thenReturn( - ConsentEvaluation( - TtpConsentStatus.BROAD_CONSENT_GIVEN, - false - ) - ) + @Test + fun shouldProcessDnpmV2Request() { + whenever(consentEvaluator.check(any())) + .thenReturn(ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, false)) - val mtbFile = Mtb.builder() + val mtbFile = + Mtb.builder() .patient(Patient.builder().id("DUMMY_12345678").build()) .metadata( - MvhMetadata - .builder() + MvhMetadata.builder() .modelProjectConsent( - ModelProjectConsent - .builder() + ModelProjectConsent.builder() .provisions( listOf( - Provision.builder().type(ConsentProvision.DENY) - .purpose(ModelProjectConsentPurpose.SEQUENCING).build() + Provision.builder() + .type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() ) - ).build() + ) + .build() ) .build() ) .build() - val headers = RecordHeaders( + val headers = + RecordHeaders( listOf( RecordHeader("requestId", UUID.randomUUID().toString().toByteArray()), - RecordHeader("contentType", CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray()) - ) - ) - kafkaInputListener.onMessage( - ConsumerRecord( - "testtopic", - 0, - 0, - -1L, - TimestampType.NO_TIMESTAMP_TYPE, - -1, - -1, - "", - this.objectMapper.writeValueAsString(mtbFile), - headers, - Optional.empty() + RecordHeader( + "contentType", + CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray(), + ), ) ) - verify(requestProcessor, times(1)).processDeletion( - anyValueClass(), anyValueClass(), eq( - TtpConsentStatus.UNKNOWN_CHECK_FILE - ) + 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(TtpConsentStatus.UNKNOWN_CHECK_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 ae9e4e2..95858f4 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt @@ -27,6 +27,8 @@ 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 java.time.Instant +import java.util.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -46,181 +48,176 @@ 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 { - private val objectMapper = ObjectMapper() - - @Nested - inner class RequestsForDnpmDataModel21 { - - private lateinit var mockMvc: MockMvc - - private lateinit var requestProcessor: RequestProcessor - private lateinit var consentEvaluator: ConsentEvaluator - - @BeforeEach - fun setup( - @Mock requestProcessor: RequestProcessor, - @Mock consentEvaluator: ConsentEvaluator - ) { - this.requestProcessor = requestProcessor - this.consentEvaluator = consentEvaluator - val controller = MtbFileRestController( - requestProcessor, - 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) - - mockMvc.post("/mtb") { - content = mtbFileContent - contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON - }.andExpect { - status { - isAccepted() - } - } - - 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>()) - } + private val objectMapper = ObjectMapper() + + @Nested + inner class RequestsForDnpmDataModel21 { + + private lateinit var mockMvc: MockMvc + + private lateinit var requestProcessor: RequestProcessor + private lateinit var consentEvaluator: ConsentEvaluator + + @BeforeEach + fun setup(@Mock requestProcessor: RequestProcessor, @Mock consentEvaluator: ConsentEvaluator) { + this.requestProcessor = requestProcessor + this.consentEvaluator = consentEvaluator + val controller = MtbFileRestController(requestProcessor, 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) + + mockMvc + .post("/mtb") { + content = mtbFileContent + contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON + } + .andExpect { status { isAccepted() } } + + 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() +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", ), - 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() + // 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() - ).build() + .build() + ), + TtpConsentStatus.UNKNOWN_CHECK_FILE, + "process", ), - 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()) + // 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() - } + .build() + ), + TtpConsentStatus.BROAD_CONSENT_GIVEN, + "process", + ), + ) { + + companion object { + fun buildMtb(metadata: MvhMetadata?): Mtb { + return Mtb.builder() + .patient( + Patient.builder() + .id("TEST_12345678") + .birthDate(Date.from(Instant.parse("2000-08-08T12:34:56Z"))) + .gender(GenderCoding.builder().code(GenderCodingCode.MALE).build()) + .build() + ) + .metadata(metadata) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("TEST_12345678").build()) + .build() + ) + ) + .build() } + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt index a6d855c..a380d2a 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ConnectionCheckServiceTest.kt @@ -22,10 +22,8 @@ import reactor.test.StepVerifier @ExtendWith(MockitoExtension::class) class ConnectionCheckServiceTest { - @Nested inner class RestConnectionCheckServiceTest { - lateinit var mockRestServiceServer: MockRestServiceServer lateinit var service: RestConnectionCheckService lateinit var sink: Sinks.Many<ConnectionCheckResult> @@ -33,11 +31,12 @@ class ConnectionCheckServiceTest { @BeforeEach fun setUp() { val restTemplate = RestTemplate() - val restTargetProperties = RestTargetProperties( - "http://localhost/api", - "user", - "password", - ) + val restTargetProperties = + RestTargetProperties( + "http://localhost/api", + "user", + "password", + ) this.sink = Sinks.many().multicast().onBackpressureBuffer() this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) @@ -65,12 +64,12 @@ class ConnectionCheckServiceTest { withSuccess("OK", MediaType.APPLICATION_JSON), ) - val verifier = StepVerifier.create(sink.asFlux()) - .assertNext { - assertThat(it.available).isTrue() - } - .expectComplete() - .verifyLater() + val verifier = + StepVerifier + .create(sink.asFlux()) + .assertNext { assertThat(it.available).isTrue() } + .expectComplete() + .verifyLater() this.service.check() @@ -81,18 +80,14 @@ class ConnectionCheckServiceTest { @Test fun shouldEmitUnavailable() { - this.mockRestServiceServer - .expect(method(HttpMethod.GET)) - .andRespond( - withServerError() - ) + this.mockRestServiceServer.expect(method(HttpMethod.GET)).andRespond(withServerError()) - val verifier = StepVerifier.create(sink.asFlux()) - .assertNext { - assertThat(it.available).isFalse() - } - .expectComplete() - .verifyLater() + val verifier = + StepVerifier + .create(sink.asFlux()) + .assertNext { assertThat(it.available).isFalse() } + .expectComplete() + .verifyLater() this.service.check() @@ -104,7 +99,6 @@ class ConnectionCheckServiceTest { @Nested inner class GPasConnectionCheckServiceTest { - lateinit var mockRestServiceServer: MockRestServiceServer lateinit var service: GPasConnectionCheckService lateinit var sink: Sinks.Many<ConnectionCheckResult> @@ -112,15 +106,16 @@ class ConnectionCheckServiceTest { @BeforeEach fun setUp() { val restTemplate = RestTemplate() - val gpasTargetProperties = GPasConfigProperties( - "http://localhost/gpas", - null, - null, - "patientDomain", - "genomDeTanDomain", - "username", - "password", - ) + val gpasTargetProperties = + GPasConfigProperties( + "http://localhost/gpas", + null, + null, + "patientDomain", + "genomDeTanDomain", + "username", + "password", + ) this.sink = Sinks.many().multicast().onBackpressureBuffer() this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) @@ -149,12 +144,12 @@ class ConnectionCheckServiceTest { withSuccess("OK", MediaType.APPLICATION_JSON), ) - val verifier = StepVerifier.create(sink.asFlux()) - .assertNext { - assertThat(it.available).isTrue() - } - .expectComplete() - .verifyLater() + val verifier = + StepVerifier + .create(sink.asFlux()) + .assertNext { assertThat(it.available).isTrue() } + .expectComplete() + .verifyLater() this.service.check() @@ -165,18 +160,14 @@ class ConnectionCheckServiceTest { @Test fun shouldEmitUnavailable() { - this.mockRestServiceServer - .expect(method(HttpMethod.GET)) - .andRespond( - withServerError() - ) + this.mockRestServiceServer.expect(method(HttpMethod.GET)).andRespond(withServerError()) - val verifier = StepVerifier.create(sink.asFlux()) - .assertNext { - assertThat(it.available).isFalse() - } - .expectComplete() - .verifyLater() + val verifier = + StepVerifier + .create(sink.asFlux()) + .assertNext { assertThat(it.available).isFalse() } + .expectComplete() + .verifyLater() this.service.check() @@ -188,7 +179,6 @@ class ConnectionCheckServiceTest { @Nested inner class GIcsConnectionCheckServiceTest { - lateinit var mockRestServiceServer: MockRestServiceServer lateinit var service: GIcsConnectionCheckService lateinit var sink: Sinks.Many<ConnectionCheckResult> @@ -197,11 +187,12 @@ class ConnectionCheckServiceTest { fun setUp() { val restTemplate = RestTemplate() - val gicsTargetProperties = GIcsConfigProperties( - "http://localhost/gics", - "username", - "password", - ) + val gicsTargetProperties = + GIcsConfigProperties( + "http://localhost/gics", + "username", + "password", + ) this.sink = Sinks.many().multicast().onBackpressureBuffer() this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) @@ -220,7 +211,6 @@ class ConnectionCheckServiceTest { this.service.check() this.mockRestServiceServer.verify() - } @Test @@ -231,12 +221,12 @@ class ConnectionCheckServiceTest { withSuccess("OK", MediaType.APPLICATION_JSON), ) - val verifier = StepVerifier.create(sink.asFlux()) - .assertNext { - assertThat(it.available).isTrue() - } - .expectComplete() - .verifyLater() + val verifier = + StepVerifier + .create(sink.asFlux()) + .assertNext { assertThat(it.available).isTrue() } + .expectComplete() + .verifyLater() this.service.check() @@ -247,18 +237,14 @@ class ConnectionCheckServiceTest { @Test fun shouldEmitUnavailable() { - this.mockRestServiceServer - .expect(method(HttpMethod.GET)) - .andRespond( - withServerError() - ) + this.mockRestServiceServer.expect(method(HttpMethod.GET)).andRespond(withServerError()) - val verifier = StepVerifier.create(sink.asFlux()) - .assertNext { - assertThat(it.available).isFalse() - } - .expectComplete() - .verifyLater() + val verifier = + StepVerifier + .create(sink.asFlux()) + .assertNext { assertThat(it.available).isFalse() } + .expectComplete() + .verifyLater() this.service.check() @@ -267,5 +253,4 @@ class ConnectionCheckServiceTest { verifier.verify() } } - -}
\ No newline at end of file +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ReportServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ReportServiceTest.kt index 4bf1321..74d1138 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ReportServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/monitoring/ReportServiceTest.kt @@ -1,36 +1,36 @@ package dev.dnpm.etl.processor.monitoring import dev.dnpm.etl.processor.config.JacksonConfig +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.util.* class ReportServiceTest { - lateinit var service: ReportService + lateinit var service: ReportService - @BeforeEach - fun setUp() { - val jacksonConfig = JacksonConfig() - service = ReportService(jacksonConfig.objectMapper()) - } + @BeforeEach + fun setUp() { + val jacksonConfig = JacksonConfig() + service = ReportService(jacksonConfig.objectMapper()) + } - @Test - fun shouldParseDataQualityReport() { - val dataQualityReport = Objects.requireNonNull(this.javaClass.classLoader.getResource("dip-response.json")) + @Test + fun shouldParseDataQualityReport() { + val dataQualityReport = + Objects.requireNonNull(this.javaClass.classLoader.getResource("dip-response.json")) .readText() - val actual = service.deserialize(dataQualityReport) - - assertThat(actual).isNotNull - assertThat(actual).hasSize(6) - assertThat(actual[0].severity).isEqualTo(ReportService.Severity.FATAL) - assertThat(actual[1].severity).isEqualTo(ReportService.Severity.ERROR) - assertThat(actual[2].severity).isEqualTo(ReportService.Severity.WARNING) - assertThat(actual[3].severity).isEqualTo(ReportService.Severity.WARNING) - assertThat(actual[4].severity).isEqualTo(ReportService.Severity.WARNING) - assertThat(actual[5].severity).isEqualTo(ReportService.Severity.INFO) - } - -}
\ No newline at end of file + val actual = service.deserialize(dataQualityReport) + + assertThat(actual).isNotNull + assertThat(actual).hasSize(6) + assertThat(actual[0].severity).isEqualTo(ReportService.Severity.FATAL) + assertThat(actual[1].severity).isEqualTo(ReportService.Severity.ERROR) + assertThat(actual[2].severity).isEqualTo(ReportService.Severity.WARNING) + assertThat(actual[3].severity).isEqualTo(ReportService.Severity.WARNING) + assertThat(actual[4].severity).isEqualTo(ReportService.Severity.WARNING) + assertThat(actual[5].severity).isEqualTo(ReportService.Severity.INFO) + } +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt index 022b8dd..b1cd5fa 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/output/KafkaMtbFileSenderTest.kt @@ -26,6 +26,10 @@ import dev.dnpm.etl.processor.RequestId import dev.dnpm.etl.processor.config.KafkaProperties import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.pcvolkmer.mv64e.mtb.* +import java.time.Instant +import java.util.* +import java.util.concurrent.CompletableFuture.completedFuture +import java.util.concurrent.ExecutionException import org.apache.kafka.clients.producer.ProducerRecord import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -41,203 +45,208 @@ import org.springframework.kafka.core.KafkaTemplate import org.springframework.kafka.support.SendResult import org.springframework.retry.policy.SimpleRetryPolicy import org.springframework.retry.support.RetryTemplateBuilder -import java.time.Instant -import java.util.* -import java.util.concurrent.CompletableFuture.completedFuture -import java.util.concurrent.ExecutionException @ExtendWith(MockitoExtension::class) class KafkaMtbFileSenderTest { - @Nested - inner class BwhcV1Record { + @Nested + inner class BwhcV1Record { - private lateinit var kafkaTemplate: KafkaTemplate<String, String> + private lateinit var kafkaTemplate: KafkaTemplate<String, String> - private lateinit var kafkaMtbFileSender: KafkaMtbFileSender + private lateinit var kafkaMtbFileSender: KafkaMtbFileSender - private lateinit var objectMapper: ObjectMapper + private lateinit var objectMapper: ObjectMapper - @BeforeEach - fun setup( - @Mock kafkaTemplate: KafkaTemplate<String, String> - ) { - val kafkaProperties = KafkaProperties("testtopic") - val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() + @BeforeEach + fun setup(@Mock kafkaTemplate: KafkaTemplate<String, String>) { + val kafkaProperties = KafkaProperties("testtopic") + val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() - this.objectMapper = ObjectMapper() - this.kafkaTemplate = kafkaTemplate + this.objectMapper = ObjectMapper() + this.kafkaTemplate = kafkaTemplate - this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper) - } - - @ParameterizedTest - @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") - fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) { - doAnswer { - if (null != testData.exception) { - throw testData.exception - } - completedFuture(SendResult<String, String>(null, null)) - }.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>()) - - val response = kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) - assertThat(response.status).isEqualTo(testData.requestStatus) - } - - @ParameterizedTest - @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") - fun shouldRetryOnDeleteKafkaSendError(testData: TestData) { - val kafkaProperties = KafkaProperties("testtopic") - val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build() - this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper) - - doAnswer { - if (null != testData.exception) { - throw testData.exception - } - completedFuture(SendResult<String, String>(null, null)) - }.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>()) - - kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) + this.kafkaMtbFileSender = + KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper) + } - val expectedCount = when (testData.exception) { - // OK - No Retry - null -> times(1) - // Request failed - Retry max 3 times - else -> times(3) + @ParameterizedTest + @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") + fun shouldSendDeleteRequestAndReturnExpectedState(testData: TestData) { + doAnswer { + if (null != testData.exception) { + throw testData.exception } + completedFuture(SendResult<String, String>(null, null)) + } + .whenever(kafkaTemplate) + .send(any<ProducerRecord<String, String>>()) - verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>()) - } - + val response = kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) + assertThat(response.status).isEqualTo(testData.requestStatus) } - @Nested - inner class DnpmV2Record { - - private lateinit var kafkaTemplate: KafkaTemplate<String, String> + @ParameterizedTest + @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") + fun shouldRetryOnDeleteKafkaSendError(testData: TestData) { + val kafkaProperties = KafkaProperties("testtopic") + val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build() + this.kafkaMtbFileSender = + KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper) + + doAnswer { + if (null != testData.exception) { + throw testData.exception + } + completedFuture(SendResult<String, String>(null, null)) + } + .whenever(kafkaTemplate) + .send(any<ProducerRecord<String, String>>()) + + kafkaMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) + + val expectedCount = + when (testData.exception) { + // OK - No Retry + null -> times(1) + // Request failed - Retry max 3 times + else -> times(3) + } + + verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>()) + } + } - private lateinit var kafkaMtbFileSender: KafkaMtbFileSender + @Nested + inner class DnpmV2Record { - private lateinit var objectMapper: ObjectMapper + private lateinit var kafkaTemplate: KafkaTemplate<String, String> - @BeforeEach - fun setup( - @Mock kafkaTemplate: KafkaTemplate<String, String> - ) { - val kafkaProperties = KafkaProperties("testtopic") - val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() + private lateinit var kafkaMtbFileSender: KafkaMtbFileSender - this.objectMapper = ObjectMapper() - this.kafkaTemplate = kafkaTemplate + private lateinit var objectMapper: ObjectMapper - this.kafkaMtbFileSender = KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper) - } + @BeforeEach + fun setup(@Mock kafkaTemplate: KafkaTemplate<String, String>) { + val kafkaProperties = KafkaProperties("testtopic") + val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() - @ParameterizedTest - @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") - fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) { - doAnswer { - if (null != testData.exception) { - throw testData.exception - } - completedFuture(SendResult<String, String>(null, null)) - }.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>()) - - val response = kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) - assertThat(response.status).isEqualTo(testData.requestStatus) - } - - @Test - fun shouldSendMtbFileRequestWithCorrectKeyAndHeaderAndBody() { - doAnswer { - completedFuture(SendResult<String, String>(null, null)) - }.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>()) - - kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) - - val captor = argumentCaptor<ProducerRecord<String, String>>() - verify(kafkaTemplate, times(1)).send(captor.capture()) - assertThat(captor.firstValue.key()).isNotNull - assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}") - assertThat(captor.firstValue.headers().headers("contentType")).isNotNull - assertThat(captor.firstValue.headers().headers("contentType")?.firstOrNull()?.value()).isEqualTo(CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray()) - assertThat(captor.firstValue.headers().headers("requestId")).isNotNull - assertThat(captor.firstValue.headers().headers("requestId")?.firstOrNull()?.value()).isEqualTo(TEST_REQUEST_ID.value.toByteArray()) - assertThat(captor.firstValue.value()).isNotNull - assertThat(captor.firstValue.value()).isEqualTo(objectMapper.writeValueAsString(dnmpV2kafkaRecordData(TEST_REQUEST_ID))) - } - - @ParameterizedTest - @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") - fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) { - val kafkaProperties = KafkaProperties("testtopic") - val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build() - this.kafkaMtbFileSender = KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper) - - doAnswer { - if (null != testData.exception) { - throw testData.exception - } - completedFuture(SendResult<String, String>(null, null)) - }.whenever(kafkaTemplate).send(any<ProducerRecord<String, String>>()) + this.objectMapper = ObjectMapper() + this.kafkaTemplate = kafkaTemplate - kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) + this.kafkaMtbFileSender = + KafkaMtbFileSender(kafkaTemplate, kafkaProperties, retryTemplate, objectMapper) + } - val expectedCount = when (testData.exception) { - // OK - No Retry - null -> times(1) - // Request failed - Retry max 3 times - else -> times(3) + @ParameterizedTest + @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") + fun shouldSendMtbFileRequestAndReturnExpectedState(testData: TestData) { + doAnswer { + if (null != testData.exception) { + throw testData.exception } + completedFuture(SendResult<String, String>(null, null)) + } + .whenever(kafkaTemplate) + .send(any<ProducerRecord<String, String>>()) - verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>()) - } + val response = kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) + assertThat(response.status).isEqualTo(testData.requestStatus) + } + @Test + fun shouldSendMtbFileRequestWithCorrectKeyAndHeaderAndBody() { + doAnswer { completedFuture(SendResult<String, String>(null, null)) } + .whenever(kafkaTemplate) + .send(any<ProducerRecord<String, String>>()) + + kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) + + val captor = argumentCaptor<ProducerRecord<String, String>>() + verify(kafkaTemplate, times(1)).send(captor.capture()) + assertThat(captor.firstValue.key()).isNotNull + assertThat(captor.firstValue.key()).isEqualTo("{\"pid\": \"PID\"}") + assertThat(captor.firstValue.headers().headers("contentType")).isNotNull + assertThat(captor.firstValue.headers().headers("contentType")?.firstOrNull()?.value()) + .isEqualTo(CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE.toByteArray()) + assertThat(captor.firstValue.headers().headers("requestId")).isNotNull + assertThat(captor.firstValue.headers().headers("requestId")?.firstOrNull()?.value()) + .isEqualTo(TEST_REQUEST_ID.value.toByteArray()) + assertThat(captor.firstValue.value()).isNotNull + assertThat(captor.firstValue.value()) + .isEqualTo(objectMapper.writeValueAsString(dnmpV2kafkaRecordData(TEST_REQUEST_ID))) } - companion object { - val TEST_REQUEST_ID = RequestId("TestId") - val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID") - - fun dnpmV2MtbFile(): Mtb { - return Mtb().apply { - this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply { - this.id = "PID" - this.birthDate = Date.from(Instant.now()) - this.gender = GenderCoding().apply { - this.code = GenderCodingCode.MALE - } - } - this.episodesOfCare = listOf( - MtbEpisodeOfCare().apply { - this.id = "1" - this.patient = Reference().apply { - this.id = "PID" - } - this.period = PeriodDate().apply { - this.start = Date.from(Instant.now()) - } - } - ) + @ParameterizedTest + @MethodSource("dev.dnpm.etl.processor.output.KafkaMtbFileSenderTest#requestWithResponseSource") + fun shouldRetryOnMtbFileKafkaSendError(testData: TestData) { + val kafkaProperties = KafkaProperties("testtopic") + val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(3)).build() + this.kafkaMtbFileSender = + KafkaMtbFileSender(this.kafkaTemplate, kafkaProperties, retryTemplate, this.objectMapper) + + doAnswer { + if (null != testData.exception) { + throw testData.exception } - } - - fun dnmpV2kafkaRecordData(requestId: RequestId): Mtb { - return DnpmV2MtbFileRequest(requestId, dnpmV2MtbFile()).content - } - - data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null) - - @JvmStatic - fun requestWithResponseSource(): Set<TestData> { - return setOf( - TestData(RequestStatus.UNKNOWN), - TestData(RequestStatus.ERROR, InterruptedException("Test interrupted")), - TestData(RequestStatus.ERROR, ExecutionException(RuntimeException("Test execution aborted"))) + completedFuture(SendResult<String, String>(null, null)) + } + .whenever(kafkaTemplate) + .send(any<ProducerRecord<String, String>>()) + + kafkaMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) + + val expectedCount = + when (testData.exception) { + // OK - No Retry + null -> times(1) + // Request failed - Retry max 3 times + else -> times(3) + } + + verify(kafkaTemplate, expectedCount).send(any<ProducerRecord<String, String>>()) + } + } + + companion object { + val TEST_REQUEST_ID = RequestId("TestId") + val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID") + + fun dnpmV2MtbFile(): Mtb { + return Mtb().apply { + this.patient = + dev.pcvolkmer.mv64e.mtb.Patient().apply { + this.id = "PID" + this.birthDate = Date.from(Instant.now()) + this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE } + } + this.episodesOfCare = + listOf( + MtbEpisodeOfCare().apply { + this.id = "1" + this.patient = Reference().apply { this.id = "PID" } + this.period = PeriodDate().apply { this.start = Date.from(Instant.now()) } + } ) - } + } } + fun dnmpV2kafkaRecordData(requestId: RequestId): Mtb { + return DnpmV2MtbFileRequest(requestId, dnpmV2MtbFile()).content + } + + data class TestData(val requestStatus: RequestStatus, val exception: Throwable? = null) + + @JvmStatic + fun requestWithResponseSource(): Set<TestData> { + return setOf( + TestData(RequestStatus.UNKNOWN), + TestData(RequestStatus.ERROR, InterruptedException("Test interrupted")), + TestData( + RequestStatus.ERROR, + ExecutionException(RuntimeException("Test execution aborted")), + ), + ) + } + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt index 1b27a62..2ab0218 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/output/RestDipMtbFileSenderTest.kt @@ -30,6 +30,8 @@ import dev.dnpm.etl.processor.config.RestTargetProperties import dev.dnpm.etl.processor.monitoring.ReportService import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.pcvolkmer.mv64e.mtb.* +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -38,7 +40,6 @@ import org.junit.jupiter.params.provider.MethodSource import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus -import org.springframework.http.MediaType import org.springframework.retry.backoff.NoBackOffPolicy import org.springframework.retry.policy.SimpleRetryPolicy import org.springframework.retry.support.RetryTemplateBuilder @@ -47,224 +48,252 @@ import org.springframework.test.web.client.MockRestServiceServer import org.springframework.test.web.client.match.MockRestRequestMatchers.* import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus import org.springframework.web.client.RestTemplate -import java.time.Instant -import java.util.* class RestDipMtbFileSenderTest { - @Nested - inner class DnpmV2ContentRequest { + @Nested + inner class DnpmV2ContentRequest { - private lateinit var mockRestServiceServer: MockRestServiceServer + private lateinit var mockRestServiceServer: MockRestServiceServer - private lateinit var restMtbFileSender: RestMtbFileSender + private lateinit var restMtbFileSender: RestMtbFileSender - private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build())) + private var reportService = + ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build())) - @BeforeEach - fun setup() { - val restTemplate = RestTemplate() - val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null) - val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() + @BeforeEach + fun setup() { + val restTemplate = RestTemplate() + val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null) + val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() - this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) - - this.restMtbFileSender = RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) - } - - @ParameterizedTest - @MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource") - fun shouldReturnExpectedResponseForDnpmV2MtbFilePost(requestWithResponse: RequestWithResponse) { - this.mockRestServiceServer - .expect(method(HttpMethod.POST)) - .andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record")) - .andExpect(header(HttpHeaders.CONTENT_TYPE, CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE)) - .andRespond { - withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it) - } + this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) - val response = restMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) - assertThat(response.status).isEqualTo(requestWithResponse.response.status) - assertThat(response.body).isEqualTo(requestWithResponse.response.body) - } + this.restMtbFileSender = + RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) + } + @ParameterizedTest + @MethodSource( + "dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#mtbFileRequestWithResponseSource" + ) + fun shouldReturnExpectedResponseForDnpmV2MtbFilePost(requestWithResponse: RequestWithResponse) { + this.mockRestServiceServer + .expect(method(HttpMethod.POST)) + .andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient-record")) + .andExpect( + header( + HttpHeaders.CONTENT_TYPE, + CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON_VALUE, + ) + ) + .andRespond { + withStatus(requestWithResponse.httpStatus) + .body(requestWithResponse.body) + .createResponse(it) + } + + val response = restMtbFileSender.send(DnpmV2MtbFileRequest(TEST_REQUEST_ID, dnpmV2MtbFile())) + assertThat(response.status).isEqualTo(requestWithResponse.response.status) + assertThat(response.body).isEqualTo(requestWithResponse.response.body) } + } - @Nested - inner class DeleteRequest { + @Nested + inner class DeleteRequest { - private lateinit var mockRestServiceServer: MockRestServiceServer + private lateinit var mockRestServiceServer: MockRestServiceServer - private lateinit var restMtbFileSender: RestMtbFileSender + private lateinit var restMtbFileSender: RestMtbFileSender - private var reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build())) + private var reportService = + ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build())) - @BeforeEach - fun setup() { - val restTemplate = RestTemplate() - val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null) - val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() + @BeforeEach + fun setup() { + val restTemplate = RestTemplate() + val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null) + val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build() - this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) + this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) - this.restMtbFileSender = - RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) - } + this.restMtbFileSender = + RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) + } - @ParameterizedTest - @MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource") - fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) { - this.mockRestServiceServer - .expect(method(HttpMethod.DELETE)) - .andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}")) - .andRespond { - withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it) - } + @ParameterizedTest + @MethodSource( + "dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource" + ) + fun shouldReturnExpectedResponseForDelete(requestWithResponse: RequestWithResponse) { + this.mockRestServiceServer + .expect(method(HttpMethod.DELETE)) + .andExpect( + requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}") + ) + .andRespond { + withStatus(requestWithResponse.httpStatus) + .body(requestWithResponse.body) + .createResponse(it) + } + + val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) + assertThat(response.status).isEqualTo(requestWithResponse.response.status) + assertThat(response.body).isEqualTo(requestWithResponse.response.body) + } - val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) - assertThat(response.status).isEqualTo(requestWithResponse.response.status) - assertThat(response.body).isEqualTo(requestWithResponse.response.body) - } - - @ParameterizedTest - @MethodSource("dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource") - fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) { - val restTemplate = RestTemplate() - val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null) - val retryTemplate = AppConfiguration().retryTemplate(AppConfigProperties()) - retryTemplate.setBackOffPolicy(NoBackOffPolicy()) - - this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) - this.restMtbFileSender = - RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) - - val expectedCount = when (requestWithResponse.httpStatus) { - // OK - No Retry - HttpStatus.OK, HttpStatus.CREATED, HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.BAD_REQUEST -> ExpectedCount.max( - 1 - ) - // Request failed - Retry max 3 times - else -> ExpectedCount.max(3) + @ParameterizedTest + @MethodSource( + "dev.dnpm.etl.processor.output.RestDipMtbFileSenderTest#deleteRequestWithResponseSource" + ) + fun shouldRetryOnDeleteHttpRequestError(requestWithResponse: RequestWithResponse) { + val restTemplate = RestTemplate() + val restTargetProperties = RestTargetProperties("http://localhost:9000/api", null, null) + val retryTemplate = AppConfiguration().retryTemplate(AppConfigProperties()) + retryTemplate.setBackOffPolicy(NoBackOffPolicy()) + + this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate) + this.restMtbFileSender = + RestDipMtbFileSender(restTemplate, restTargetProperties, retryTemplate, reportService) + + val expectedCount = + when (requestWithResponse.httpStatus) { + // OK - No Retry + HttpStatus.OK, + HttpStatus.CREATED, + HttpStatus.UNPROCESSABLE_ENTITY, + HttpStatus.BAD_REQUEST -> ExpectedCount.max(1) + // Request failed - Retry max 3 times + else -> ExpectedCount.max(3) + } + + this.mockRestServiceServer + .expect(expectedCount, method(HttpMethod.DELETE)) + .andExpect( + requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}") + ) + .andRespond { + withStatus(requestWithResponse.httpStatus) + .body(requestWithResponse.body) + .createResponse(it) + } + + val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) + assertThat(response.status).isEqualTo(requestWithResponse.response.status) + assertThat(response.body).isEqualTo(requestWithResponse.response.body) + } + } + + companion object { + data class RequestWithResponse( + val httpStatus: HttpStatus, + val body: String, + val response: MtbFileSender.Response, + ) + + val TEST_REQUEST_ID = RequestId("TestId") + val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID") + + fun dnpmV2MtbFile(): Mtb { + return Mtb().apply { + this.patient = + dev.pcvolkmer.mv64e.mtb.Patient().apply { + this.id = "PID" + this.birthDate = Date.from(Instant.now()) + this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE } } - - this.mockRestServiceServer - .expect(expectedCount, method(HttpMethod.DELETE)) - .andExpect(requestTo("http://localhost:9000/api/mtb/etl/patient/${TEST_PATIENT_PSEUDONYM.value}")) - .andRespond { - withStatus(requestWithResponse.httpStatus).body(requestWithResponse.body).createResponse(it) + this.episodesOfCare = + listOf( + MtbEpisodeOfCare().apply { + this.id = "1" + this.patient = Reference().apply { this.id = "PID" } + this.period = PeriodDate().apply { this.start = Date.from(Instant.now()) } } + ) + } + } - val response = restMtbFileSender.send(DeleteRequest(TEST_REQUEST_ID, TEST_PATIENT_PSEUDONYM)) - assertThat(response.status).isEqualTo(requestWithResponse.response.status) - assertThat(response.body).isEqualTo(requestWithResponse.response.body) - } - + private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung" + + /** + * Synthetic http responses with related request status Also see: + * https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API + */ + @JvmStatic + fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> { + return setOf( + RequestWithResponse( + HttpStatus.OK, + responseBodyWithMaxSeverity(ReportService.Severity.INFO), + MtbFileSender.Response( + RequestStatus.SUCCESS, + responseBodyWithMaxSeverity(ReportService.Severity.INFO), + ), + ), + RequestWithResponse( + HttpStatus.CREATED, + responseBodyWithMaxSeverity(ReportService.Severity.WARNING), + MtbFileSender.Response( + RequestStatus.WARNING, + responseBodyWithMaxSeverity(ReportService.Severity.WARNING), + ), + ), + RequestWithResponse( + HttpStatus.BAD_REQUEST, + responseBodyWithMaxSeverity(ReportService.Severity.ERROR), + MtbFileSender.Response( + RequestStatus.ERROR, + responseBodyWithMaxSeverity(ReportService.Severity.ERROR), + ), + ), + RequestWithResponse( + HttpStatus.UNPROCESSABLE_ENTITY, + responseBodyWithMaxSeverity(ReportService.Severity.ERROR), + MtbFileSender.Response( + RequestStatus.ERROR, + responseBodyWithMaxSeverity(ReportService.Severity.ERROR), + ), + ), + // Some more errors not mentioned in documentation + RequestWithResponse( + HttpStatus.NOT_FOUND, + ERROR_RESPONSE_BODY, + MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY), + ), + RequestWithResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + ERROR_RESPONSE_BODY, + MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY), + ), + ) } - companion object { - data class RequestWithResponse( - val httpStatus: HttpStatus, - val body: String, - val response: MtbFileSender.Response - ) - - val TEST_REQUEST_ID = RequestId("TestId") - val TEST_PATIENT_PSEUDONYM = PatientPseudonym("PID") - - fun dnpmV2MtbFile(): Mtb { - return Mtb().apply { - this.patient = dev.pcvolkmer.mv64e.mtb.Patient().apply { - this.id = "PID" - this.birthDate = Date.from(Instant.now()) - this.gender = GenderCoding().apply { - this.code = GenderCodingCode.MALE - } - } - this.episodesOfCare = listOf( - MtbEpisodeOfCare().apply { - this.id = "1" - this.patient = Reference().apply { - this.id = "PID" - } - this.period = PeriodDate().apply { - this.start = Date.from(Instant.now()) - } - } - ) - } - } - - private const val ERROR_RESPONSE_BODY = "Sonstiger Fehler bei der Übertragung" - - /** - * Synthetic http responses with related request status - * Also see: https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API - */ - @JvmStatic - fun mtbFileRequestWithResponseSource(): Set<RequestWithResponse> { - return setOf( - RequestWithResponse( - HttpStatus.OK, - responseBodyWithMaxSeverity(ReportService.Severity.INFO), - MtbFileSender.Response( - RequestStatus.SUCCESS, - responseBodyWithMaxSeverity(ReportService.Severity.INFO) - ) - ), - RequestWithResponse( - HttpStatus.CREATED, - responseBodyWithMaxSeverity(ReportService.Severity.WARNING), - MtbFileSender.Response(RequestStatus.WARNING, responseBodyWithMaxSeverity(ReportService.Severity.WARNING)) - ), - RequestWithResponse( - HttpStatus.BAD_REQUEST, - responseBodyWithMaxSeverity(ReportService.Severity.ERROR), - MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR)) - ), - RequestWithResponse( - HttpStatus.UNPROCESSABLE_ENTITY, - responseBodyWithMaxSeverity(ReportService.Severity.ERROR), - MtbFileSender.Response(RequestStatus.ERROR, responseBodyWithMaxSeverity(ReportService.Severity.ERROR)) - ), - // Some more errors not mentioned in documentation - RequestWithResponse( - HttpStatus.NOT_FOUND, - ERROR_RESPONSE_BODY, - MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY) - ), - RequestWithResponse( - HttpStatus.INTERNAL_SERVER_ERROR, - ERROR_RESPONSE_BODY, - MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY) - ) - ) - } - - /** - * Synthetic http responses with related request status - * Also see: https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API - */ - @JvmStatic - fun deleteRequestWithResponseSource(): Set<RequestWithResponse> { - return setOf( - RequestWithResponse(HttpStatus.OK, "", MtbFileSender.Response(RequestStatus.SUCCESS)), - // Some more errors not mentioned in documentation - RequestWithResponse( - HttpStatus.NOT_FOUND, - "what????", - MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY) - ), - RequestWithResponse( - HttpStatus.INTERNAL_SERVER_ERROR, - "what????", - MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY) - ) - ) - } + /** + * Synthetic http responses with related request status Also see: + * https://ibmi-intra.cs.uni-tuebingen.de/display/ZPM/bwHC+REST+API + */ + @JvmStatic + fun deleteRequestWithResponseSource(): Set<RequestWithResponse> { + return setOf( + RequestWithResponse(HttpStatus.OK, "", MtbFileSender.Response(RequestStatus.SUCCESS)), + // Some more errors not mentioned in documentation + RequestWithResponse( + HttpStatus.NOT_FOUND, + "what????", + MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY), + ), + RequestWithResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "what????", + MtbFileSender.Response(RequestStatus.ERROR, ERROR_RESPONSE_BODY), + ), + ) + } - fun responseBodyWithMaxSeverity(severity: ReportService.Severity): String { - return when (severity) { - ReportService.Severity.INFO -> """ + fun responseBodyWithMaxSeverity(severity: ReportService.Severity): String { + return when (severity) { + ReportService.Severity.INFO -> + """ { "patient": "PID", "issues": [ @@ -273,7 +302,8 @@ class RestDipMtbFileSenderTest { } """ - ReportService.Severity.WARNING -> """ + ReportService.Severity.WARNING -> + """ { "patient": "PID", "issues": [ @@ -283,7 +313,8 @@ class RestDipMtbFileSenderTest { } """ - ReportService.Severity.ERROR -> """ + ReportService.Severity.ERROR -> + """ { "patient": "PID", "issues": [ @@ -294,7 +325,8 @@ class RestDipMtbFileSenderTest { } """ - ReportService.Severity.FATAL -> """ + ReportService.Severity.FATAL -> + """ { "patient": "PID", "issues": [ @@ -305,9 +337,7 @@ class RestDipMtbFileSenderTest { ] } """ - } - } + } } - - + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt index c302362..a0b3b24 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/ExtensionsTest.kt @@ -28,6 +28,8 @@ 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.* +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.hl7.fhir.r4.model.Bundle import org.junit.jupiter.api.Nested @@ -40,231 +42,212 @@ import org.mockito.kotlin.anyValueClass import org.mockito.kotlin.doAnswer import org.mockito.kotlin.whenever import org.springframework.core.io.ClassPathResource -import java.time.Instant -import java.util.* @ExtendWith(MockitoExtension::class) class ExtensionsTest { - fun getObjectMapper(): ObjectMapper { - return JacksonConfig().objectMapper() - } - - @Nested - inner class UsingDnpmV2Datamodel { + fun getObjectMapper(): ObjectMapper { + return JacksonConfig().objectMapper() + } - val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json" - val CLEAN_PATIENT_ID = "644bae7a-56f6-4ee8-b02f-c532e65af5b1" + @Nested + inner class UsingDnpmV2Datamodel { - private fun fakeMtbFile(): Mtb { - val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream - return getObjectMapper().readValue(mtbFile, Mtb::class.java) - } + val FAKE_MTB_FILE_PATH = "mv64e-mtb-fake-patient.json" + val CLEAN_PATIENT_ID = "644bae7a-56f6-4ee8-b02f-c532e65af5b1" - private fun Mtb.serialized(): String { - return getObjectMapper().writeValueAsString(this) - } - - @Test - fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) { - doAnswer { - it.arguments[0] - "PSEUDO-ID" - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - val mtbFile = fakeMtbFile() - mtbFile.ensureMetaDataIsInitialized() - addConsentData(mtbFile) + private fun fakeMtbFile(): Mtb { + val mtbFile = ClassPathResource(FAKE_MTB_FILE_PATH).inputStream + return getObjectMapper().readValue(mtbFile, Mtb::class.java) + } - mtbFile.pseudonymizeWith(pseudonymizeService) + private fun Mtb.serialized(): String { + return getObjectMapper().writeValueAsString(this) + } - assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID") - assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID) - } + @Test + fun shouldNotContainCleanPatientId(@Mock pseudonymizeService: PseudonymizeService) { + doAnswer { + it.arguments[0] + "PSEUDO-ID" + } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) - private fun addConsentData(mtbFile: Mtb) { - val gIcsConfigProperties = GIcsConfigProperties("", "", "") - val appConfigProperties = AppConfigProperties(emptyList()) + val mtbFile = fakeMtbFile() + mtbFile.ensureMetaDataIsInitialized() + addConsentData(mtbFile) - val bundle = Bundle() - val dummyConsent = ConsentProcessorTest.getDummyGenomDeConsent() - dummyConsent.patient.reference = "Patient/$CLEAN_PATIENT_ID" - bundle.addEntry().resource = dummyConsent + mtbFile.pseudonymizeWith(pseudonymizeService) - ConsentProcessor( - appConfigProperties, - gIcsConfigProperties, - JacksonConfig().objectMapper(), - FhirContext.forR4(), - MtbFileConsentService() - ).embedBroadConsentResources(mtbFile, bundle) + assertThat(mtbFile.patient.id).isEqualTo("PSEUDO-ID") + assertThat(mtbFile.serialized()).doesNotContain(CLEAN_PATIENT_ID) + } - } + private fun addConsentData(mtbFile: Mtb) { + val gIcsConfigProperties = GIcsConfigProperties("", "", "") + val appConfigProperties = AppConfigProperties(emptyList()) + + val bundle = Bundle() + val dummyConsent = ConsentProcessorTest.getDummyGenomDeConsent() + dummyConsent.patient.reference = "Patient/$CLEAN_PATIENT_ID" + bundle.addEntry().resource = dummyConsent + + ConsentProcessor( + appConfigProperties, + gIcsConfigProperties, + JacksonConfig().objectMapper(), + FhirContext.forR4(), + MtbFileConsentService(), + ) + .embedBroadConsentResources(mtbFile, bundle) + } - @Test - fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) { - doAnswer { - it.arguments[0] - "PSEUDO-ID" - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - "TESTDOMAIN" - }.whenever(pseudonymizeService).prefix() - - val mtbFile = Mtb().apply { - this.patient = Patient().apply { - this.id = "PID" - this.birthDate = Date.from(Instant.now()) - this.gender = GenderCoding().apply { - this.code = GenderCodingCode.MALE - } + @Test + fun shouldNotThrowExceptionOnNullValues(@Mock pseudonymizeService: PseudonymizeService) { + doAnswer { + it.arguments[0] + "PSEUDO-ID" + } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { "TESTDOMAIN" }.whenever(pseudonymizeService).prefix() + + val mtbFile = + Mtb().apply { + this.patient = + Patient().apply { + this.id = "PID" + this.birthDate = Date.from(Instant.now()) + this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE } } - this.episodesOfCare = listOf( + this.episodesOfCare = + listOf( MtbEpisodeOfCare().apply { - this.id = "1" - this.patient = Reference().apply { - this.id = "PID" - } - this.period = PeriodDate().apply { - this.start = Date.from(Instant.now()) - } + this.id = "1" + this.patient = Reference().apply { this.id = "PID" } + this.period = PeriodDate().apply { this.start = Date.from(Instant.now()) } } ) - } + } - mtbFile.pseudonymizeWith(pseudonymizeService) - mtbFile.anonymizeContentWith(pseudonymizeService) + mtbFile.pseudonymizeWith(pseudonymizeService) + mtbFile.anonymizeContentWith(pseudonymizeService) - assertThat(mtbFile.episodesOfCare).hasSize(1) - assertThat(mtbFile.episodesOfCare.map { it.id }).isNotNull - } - - @Test - fun shouldNotContainAnyUuidAfterRehashingOfIds(@Mock pseudonymizeService: PseudonymizeService) { - doAnswer { - it.arguments[0] - "PSEUDO-ID" - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - "TESTDOMAIN" - }.whenever(pseudonymizeService).prefix() - - val mtbFile = fakeMtbFile() - - /** - * replace hex values with random long, so our test does not match false positives - */ - mtbFile.ngsReports.forEach { report -> - report.results.simpleVariants.forEach { simpleVariant -> - simpleVariant.externalIds.forEach { extIdValue -> - extIdValue.value = - Math.random().toLong().toString() - } - } - } - mtbFile.ngsReports.forEach { report -> - report.results.rnaFusions.forEach { simpleVariant -> - simpleVariant.externalIds.forEach { extIdValue -> - extIdValue.value = - Math.random().toLong().toString() - } - simpleVariant.fusionPartner3Prime?.transcriptId?.value = - Math.random().toLong().toString() - simpleVariant.fusionPartner5Prime?.transcriptId?.value = - Math.random().toLong().toString() - simpleVariant.externalIds?.forEach { - it?.value = Math.random().toLong().toString() - } - } - } - - mtbFile.pseudonymizeWith(pseudonymizeService) - mtbFile.anonymizeContentWith(pseudonymizeService) - - val pattern = - "\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex() - .toPattern() - val input = mtbFile.serialized() - val matcher = pattern.matcher(input) - - assertThrows<IllegalStateException> { - matcher.find() - val posSt = "check at pos: " + matcher.start().toString() + ", " + matcher.end() - println(posSt + " with " + matcher.group()) - }.also { - assertThat(it.message).isEqualTo("No match found") - } - } + assertThat(mtbFile.episodesOfCare).hasSize(1) + assertThat(mtbFile.episodesOfCare.map { it.id }).isNotNull } @Test - fun shouldUseSameAnonymIdForDiagnosisAndDiagnosisReferences(@Mock pseudonymizeService: PseudonymizeService) { - - doAnswer { + fun shouldNotContainAnyUuidAfterRehashingOfIds(@Mock pseudonymizeService: PseudonymizeService) { + doAnswer { it.arguments[0] "PSEUDO-ID" - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { "TESTDOMAIN" }.whenever(pseudonymizeService).prefix() - doAnswer { - "TESTDOMAIN" - }.whenever(pseudonymizeService).prefix() + val mtbFile = fakeMtbFile() - val mtbFile = Mtb().apply { - this.patient = Patient().apply { + /** replace hex values with random long, so our test does not match false positives */ + mtbFile.ngsReports.forEach { report -> + report.results.simpleVariants.forEach { simpleVariant -> + simpleVariant.externalIds.forEach { extIdValue -> + extIdValue.value = Math.random().toLong().toString() + } + } + } + mtbFile.ngsReports.forEach { report -> + report.results.rnaFusions.forEach { simpleVariant -> + simpleVariant.externalIds.forEach { extIdValue -> + extIdValue.value = Math.random().toLong().toString() + } + simpleVariant.fusionPartner3Prime?.transcriptId?.value = Math.random().toLong().toString() + simpleVariant.fusionPartner5Prime?.transcriptId?.value = Math.random().toLong().toString() + simpleVariant.externalIds?.forEach { it?.value = Math.random().toLong().toString() } + } + } + + mtbFile.pseudonymizeWith(pseudonymizeService) + mtbFile.anonymizeContentWith(pseudonymizeService) + + val pattern = + "\"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"".toRegex().toPattern() + val input = mtbFile.serialized() + val matcher = pattern.matcher(input) + + assertThrows<IllegalStateException> { + matcher.find() + val posSt = "check at pos: " + matcher.start().toString() + ", " + matcher.end() + println(posSt + " with " + matcher.group()) + } + .also { assertThat(it.message).isEqualTo("No match found") } + } + } + + @Test + fun shouldUseSameAnonymIdForDiagnosisAndDiagnosisReferences( + @Mock pseudonymizeService: PseudonymizeService + ) { + + doAnswer { + it.arguments[0] + "PSEUDO-ID" + } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { "TESTDOMAIN" }.whenever(pseudonymizeService).prefix() + + val mtbFile = + Mtb().apply { + this.patient = + Patient().apply { this.id = "PID" this.birthDate = Date.from(Instant.now()) - this.gender = GenderCoding().apply { - this.code = GenderCodingCode.MALE - } - } - this.diagnoses = listOf( - MtbDiagnosis().apply { - this.id = "Diagnosis-1" - } - ) - this.episodesOfCare = listOf( - MtbEpisodeOfCare().apply { + this.gender = GenderCoding().apply { this.code = GenderCodingCode.MALE } + } + this.diagnoses = listOf(MtbDiagnosis().apply { this.id = "Diagnosis-1" }) + this.episodesOfCare = + listOf( + MtbEpisodeOfCare().apply { this.id = "Episode-1" - this.diagnoses = listOf( - Reference().apply { - this.id = "Diagnosis-1" - } - ) - } - ) - this.guidelineTherapies = listOf( - MtbSystemicTherapy().apply { + this.diagnoses = listOf(Reference().apply { this.id = "Diagnosis-1" }) + } + ) + this.guidelineTherapies = + listOf( + MtbSystemicTherapy().apply { this.id = "Systemic-Therapy-1" - this.reason = Reference().apply { - this.id = "Diagnosis-1" - } - } - ) - this.guidelineProcedures = listOf( - OncoProcedure().apply { + this.reason = Reference().apply { this.id = "Diagnosis-1" } + } + ) + this.guidelineProcedures = + listOf( + OncoProcedure().apply { this.id = "Onco-Procedure-1" - this.reason = Reference().apply { - this.id = "Diagnosis-1" - } - } - ) - this.specimens = listOf( - TumorSpecimen().apply { + this.reason = Reference().apply { this.id = "Diagnosis-1" } + } + ) + this.specimens = + listOf( + TumorSpecimen().apply { this.id = "Specimen-1" - this.diagnosis = Reference().apply { - this.id = "Diagnosis-1" - } - } - ) + this.diagnosis = Reference().apply { this.id = "Diagnosis-1" } + } + ) } - mtbFile.pseudonymizeWith(pseudonymizeService) - mtbFile.anonymizeContentWith(pseudonymizeService) + mtbFile.pseudonymizeWith(pseudonymizeService) + mtbFile.anonymizeContentWith(pseudonymizeService) - assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.episodesOfCare.first().diagnoses.first().id) - assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.guidelineTherapies.first().reason.id) - assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.guidelineProcedures.first().reason.id) - assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.specimens.first().diagnosis.id) - } + assertThat(mtbFile.diagnoses.first().id) + .isEqualTo(mtbFile.episodesOfCare.first().diagnoses.first().id) + assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.guidelineTherapies.first().reason.id) + assertThat(mtbFile.diagnoses.first().id) + .isEqualTo(mtbFile.guidelineProcedures.first().reason.id) + assertThat(mtbFile.diagnoses.first().id).isEqualTo(mtbFile.specimens.first().diagnosis.id) + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt index 8ee19bc..7da0247 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/pseudonym/PseudonymizeServiceTest.kt @@ -21,6 +21,8 @@ package dev.dnpm.etl.processor.pseudonym import dev.dnpm.etl.processor.config.PseudonymizeConfigProperties import dev.pcvolkmer.mv64e.mtb.* +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -29,72 +31,67 @@ import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.doAnswer import org.mockito.kotlin.whenever -import java.time.Instant -import java.util.* @ExtendWith(MockitoExtension::class) class PseudonymizeServiceTest { - private val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .id("123") - .build() - ) - .episodesOfCare( - listOf( - MtbEpisodeOfCare.builder() - .id("1") - .patient(Reference.builder().id("123").build()) - .period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build()) - .build() - ) - ) - .build() - - @Test - fun shouldNotUsePseudonymPrefixForGpas(@Mock generator: GpasPseudonymGenerator) { - doAnswer { - it.arguments[0] - }.whenever(generator).generate(anyString()) - - val pseudonymizeService = PseudonymizeService(generator, PseudonymizeConfigProperties()) - - mtbFile.pseudonymizeWith(pseudonymizeService) - - assertThat(mtbFile.patient.id).isEqualTo("123") - } + private val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) + .episodesOfCare( + listOf( + MtbEpisodeOfCare.builder() + .id("1") + .patient(Reference.builder().id("123").build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) + .build() + ) + ) + .build() - @Test - fun sanitizeFileName() { - val result = GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>") + @Test + fun shouldNotUsePseudonymPrefixForGpas(@Mock generator: GpasPseudonymGenerator) { + doAnswer { it.arguments[0] }.whenever(generator).generate(anyString()) - assertThat(result).isEqualTo("l___a_bs_1_2_3_") - } + val pseudonymizeService = PseudonymizeService(generator, PseudonymizeConfigProperties()) - @Test - fun shouldUsePseudonymPrefixForBuiltin(@Mock generator: AnonymizingGenerator) { - doAnswer { - it.arguments[0] - }.whenever(generator).generate(anyString()) + mtbFile.pseudonymizeWith(pseudonymizeService) - val pseudonymizeService = PseudonymizeService(generator, PseudonymizeConfigProperties()) + assertThat(mtbFile.patient.id).isEqualTo("123") + } - mtbFile.pseudonymizeWith(pseudonymizeService) + @Test + fun sanitizeFileName() { + val result = GpasPseudonymGenerator.sanitizeValue("l://a\\bs;1*2?3>") - assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123") - } + assertThat(result).isEqualTo("l___a_bs_1_2_3_") + } + + @Test + fun shouldUsePseudonymPrefixForBuiltin(@Mock generator: AnonymizingGenerator) { + doAnswer { it.arguments[0] }.whenever(generator).generate(anyString()) + + val pseudonymizeService = PseudonymizeService(generator, PseudonymizeConfigProperties()) + + mtbFile.pseudonymizeWith(pseudonymizeService) + + assertThat(mtbFile.patient.id).isEqualTo("UNKNOWN_123") + } - @Test - fun shouldReturnDifferentValues() { - val ag = AnonymizingGenerator() + @Test + fun shouldReturnDifferentValues() { + val ag = AnonymizingGenerator() - val tans = HashSet<String>() + val tans = HashSet<String>() - (1..1000).forEach { i -> - val tan = ag.generateGenomDeTan("12345") - assertThat(tan).hasSize(64) - assertThat(tans.add(tan)).`as`("never the same result!").isTrue - } + (1..1000).forEach { i -> + val tan = ag.generateGenomDeTan("12345") + assertThat(tan).hasSize(64) + assertThat(tans.add(tan)).`as`("never the same result!").isTrue } + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt index b93e9f5..e9a1650 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/security/TokenServiceTest.kt @@ -19,6 +19,8 @@ package dev.dnpm.etl.processor.security +import java.util.* +import java.util.function.Consumer import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -30,124 +32,128 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.* import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.provisioning.InMemoryUserDetailsManager -import java.util.* -import java.util.function.Consumer @ExtendWith(MockitoExtension::class) class TokenServiceTest { - private lateinit var userDetailsManager: InMemoryUserDetailsManager - private lateinit var passwordEncoder: PasswordEncoder - private lateinit var tokenRepository: TokenRepository + private lateinit var userDetailsManager: InMemoryUserDetailsManager + private lateinit var passwordEncoder: PasswordEncoder + private lateinit var tokenRepository: TokenRepository - private lateinit var tokenService: TokenService + private lateinit var tokenService: TokenService - @BeforeEach - fun setup( - @Mock userDetailsManager: InMemoryUserDetailsManager, - @Mock passwordEncoder: PasswordEncoder, - @Mock tokenRepository: TokenRepository - ) { - this.userDetailsManager = userDetailsManager - this.passwordEncoder = passwordEncoder - this.tokenRepository = tokenRepository + @BeforeEach + fun setup( + @Mock userDetailsManager: InMemoryUserDetailsManager, + @Mock passwordEncoder: PasswordEncoder, + @Mock tokenRepository: TokenRepository, + ) { + this.userDetailsManager = userDetailsManager + this.passwordEncoder = passwordEncoder + this.tokenRepository = tokenRepository - this.tokenService = TokenService(userDetailsManager, passwordEncoder, tokenRepository) - } + this.tokenService = TokenService(userDetailsManager, passwordEncoder, tokenRepository) + } - @Test - fun shouldEncodePasswordForNewToken() { - doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) + @Test + fun shouldEncodePasswordForNewToken() { + doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) - val actual = this.tokenService.addToken("Test Token") + val actual = this.tokenService.addToken("Test Token") - assertThat(actual).satisfies( + assertThat(actual) + .satisfies( Consumer { assertThat(it.isSuccess).isTrue() }, - Consumer { assertThat(it.getOrNull()).matches("testtoken:[A-Za-z0-9]{48}$") } + Consumer { assertThat(it.getOrNull()).matches("testtoken:[A-Za-z0-9]{48}$") }, ) - } + } - @Test - fun shouldContainAlphanumTokenUserPart() { - doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) + @Test + fun shouldContainAlphanumTokenUserPart() { + doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) - val actual = this.tokenService.addToken("Test Token") + val actual = this.tokenService.addToken("Test Token") - assertThat(actual).satisfies( + assertThat(actual) + .satisfies( Consumer { assertThat(it.isSuccess).isTrue() }, - Consumer { assertThat(it.getOrDefault("")).startsWith("testtoken:") } + Consumer { assertThat(it.getOrDefault("")).startsWith("testtoken:") }, ) - } + } - @Test - fun shouldNotAllowSameTokenUserPartTwice() { - doReturn(true).whenever(userDetailsManager).userExists(anyString()) + @Test + fun shouldNotAllowSameTokenUserPartTwice() { + doReturn(true).whenever(userDetailsManager).userExists(anyString()) - val actual = this.tokenService.addToken("Test Token") + val actual = this.tokenService.addToken("Test Token") - assertThat(actual).satisfies(Consumer { assertThat(it.isFailure).isTrue() }) - verify(tokenRepository, never()).save(any()) - } + assertThat(actual).satisfies(Consumer { assertThat(it.isFailure).isTrue() }) + verify(tokenRepository, never()).save(any()) + } - @Test - fun shouldSaveNewToken() { - doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) + @Test + fun shouldSaveNewToken() { + doAnswer { "{test}verysecret" }.whenever(passwordEncoder).encode(anyString()) - val actual = this.tokenService.addToken("Test Token") + val actual = this.tokenService.addToken("Test Token") - val captor = argumentCaptor<Token>() - verify(tokenRepository, times(1)).save(captor.capture()) + val captor = argumentCaptor<Token>() + verify(tokenRepository, times(1)).save(captor.capture()) - assertThat(actual).satisfies(Consumer { assertThat(it.isSuccess).isTrue() }) - assertThat(captor.firstValue).satisfies( + assertThat(actual).satisfies(Consumer { assertThat(it.isSuccess).isTrue() }) + assertThat(captor.firstValue) + .satisfies( Consumer { assertThat(it.name).isEqualTo("Test Token") }, Consumer { assertThat(it.username).isEqualTo("testtoken") }, - Consumer { assertThat(it.password).isEqualTo("{test}verysecret") } + Consumer { assertThat(it.password).isEqualTo("{test}verysecret") }, ) - } - - @Test - fun shouldDeleteExistingToken() { - doAnswer { - val id = it.arguments[0] as Long - Optional.of(Token(id, "Test Token", "testtoken", "{test}hsdajfgadskjhfgsdkfjg")) - }.whenever(tokenRepository).findById(anyLong()) - - this.tokenService.deleteToken(42) - - val stringCaptor = argumentCaptor<String>() - verify(userDetailsManager, times(1)).deleteUser(stringCaptor.capture()) - assertThat(stringCaptor.firstValue).isEqualTo("testtoken") - - val tokenCaptor = argumentCaptor<Token>() - verify(tokenRepository, times(1)).delete(tokenCaptor.capture()) - assertThat(tokenCaptor.firstValue.id).isEqualTo(42) - } - - @Test - fun shouldReturnAllTokensFromRepository() { - val expected = listOf( + } + + @Test + fun shouldDeleteExistingToken() { + doAnswer { + val id = it.arguments[0] as Long + Optional.of(Token(id, "Test Token", "testtoken", "{test}hsdajfgadskjhfgsdkfjg")) + } + .whenever(tokenRepository) + .findById(anyLong()) + + this.tokenService.deleteToken(42) + + val stringCaptor = argumentCaptor<String>() + verify(userDetailsManager, times(1)).deleteUser(stringCaptor.capture()) + assertThat(stringCaptor.firstValue).isEqualTo("testtoken") + + val tokenCaptor = argumentCaptor<Token>() + verify(tokenRepository, times(1)).delete(tokenCaptor.capture()) + assertThat(tokenCaptor.firstValue.id).isEqualTo(42) + } + + @Test + fun shouldReturnAllTokensFromRepository() { + val expected = + listOf( Token(1, "Test Token 1", "testtoken1", "{test}hsdajfgadskjhfgsdkfjg"), - Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd") + Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd"), ) - doReturn(expected).whenever(tokenRepository).findAll() + doReturn(expected).whenever(tokenRepository).findAll() - assertThat(tokenService.findAll()).isEqualTo(expected) - } + assertThat(tokenService.findAll()).isEqualTo(expected) + } - @Test - fun shouldAddAllTokensFromRepositoryToUserDataManager() { - val expected = listOf( + @Test + fun shouldAddAllTokensFromRepositoryToUserDataManager() { + val expected = + listOf( Token(1, "Test Token 1", "testtoken1", "{test}hsdajfgadskjhfgsdkfjg"), - Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd") + Token(2, "Test Token 2", "testtoken2", "{test}asdasdasdasdasdasdasd"), ) - doReturn(expected).whenever(tokenRepository).findAll() - - tokenService.setup() + doReturn(expected).whenever(tokenRepository).findAll() - verify(userDetailsManager, times(expected.size)).createUser(any()) - } + tokenService.setup() -}
\ No newline at end of file + verify(userDetailsManager, times(expected.size)).createUser(any()) + } +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt index 39ba7c0..7743069 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/security/UserRoleServiceTest.kt @@ -19,6 +19,8 @@ package dev.dnpm.etl.processor.security +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -32,161 +34,148 @@ import org.springframework.security.core.session.SessionRegistry import org.springframework.security.oauth2.core.oidc.OidcIdToken import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser import org.springframework.security.oauth2.core.oidc.user.OidcUser -import java.time.Instant -import java.util.* @ExtendWith(MockitoExtension::class) class UserRoleServiceTest { - private lateinit var userRoleRepository: UserRoleRepository - private lateinit var sessionRegistry: SessionRegistry + private lateinit var userRoleRepository: UserRoleRepository + private lateinit var sessionRegistry: SessionRegistry - private lateinit var userRoleService: UserRoleService + private lateinit var userRoleService: UserRoleService - @BeforeEach - fun setup( - @Mock userRoleRepository: UserRoleRepository, - @Mock sessionRegistry: SessionRegistry - ) { - this.userRoleRepository = userRoleRepository - this.sessionRegistry = sessionRegistry - - this.userRoleService = UserRoleService(userRoleRepository, sessionRegistry) - } + @BeforeEach + fun setup(@Mock userRoleRepository: UserRoleRepository, @Mock sessionRegistry: SessionRegistry) { + this.userRoleRepository = userRoleRepository + this.sessionRegistry = sessionRegistry - @Test - fun shouldDelegateFindAllToRepository() { - userRoleService.findAll() + this.userRoleService = UserRoleService(userRoleRepository, sessionRegistry) + } - verify(userRoleRepository, times(1)).findAll() - } + @Test + fun shouldDelegateFindAllToRepository() { + userRoleService.findAll() - @Nested - inner class WithExistingUserRole { + verify(userRoleRepository, times(1)).findAll() + } - @BeforeEach - fun setup() { - doAnswer { invocation -> - Optional.of( - UserRole(invocation.getArgument(0), "patrick.tester", Role.USER) - ) - }.whenever(userRoleRepository).findById(any<Long>()) + @Nested + inner class WithExistingUserRole { - doAnswer { _ -> - listOf( - dummyPrincipal() - ) - }.whenever(sessionRegistry).allPrincipals - } + @BeforeEach + fun setup() { + doAnswer { invocation -> + Optional.of(UserRole(invocation.getArgument(0), "patrick.tester", Role.USER)) + } + .whenever(userRoleRepository) + .findById(any<Long>()) + + doAnswer { _ -> listOf(dummyPrincipal()) }.whenever(sessionRegistry).allPrincipals + } - @Test - fun shouldUpdateUserRole() { - userRoleService.updateUserRole(1, Role.ADMIN) + @Test + fun shouldUpdateUserRole() { + userRoleService.updateUserRole(1, Role.ADMIN) - val userRoleCaptor = argumentCaptor<UserRole>() - verify(userRoleRepository, times(1)).save(userRoleCaptor.capture()) + val userRoleCaptor = argumentCaptor<UserRole>() + verify(userRoleRepository, times(1)).save(userRoleCaptor.capture()) - assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) - assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.ADMIN) - } + assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) + assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.ADMIN) + } - @Test - fun shouldExpireSessionOnUpdate() { - val dummySessions = dummySessions() - whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn( - dummySessions - ) + @Test + fun shouldExpireSessionOnUpdate() { + val dummySessions = dummySessions() + whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn(dummySessions) - assertThat(dummySessions.filter { it.isExpired }).hasSize(0) + assertThat(dummySessions.filter { it.isExpired }).hasSize(0) - userRoleService.updateUserRole(1, Role.ADMIN) + userRoleService.updateUserRole(1, Role.ADMIN) - verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>()) + verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>()) - assertThat(dummySessions.filter { it.isExpired }).hasSize(2) - } + assertThat(dummySessions.filter { it.isExpired }).hasSize(2) + } - @Test - fun shouldDeleteUserRole() { - userRoleService.deleteUserRole(1) + @Test + fun shouldDeleteUserRole() { + userRoleService.deleteUserRole(1) - val userRoleCaptor = argumentCaptor<UserRole>() - verify(userRoleRepository, times(1)).delete(userRoleCaptor.capture()) + val userRoleCaptor = argumentCaptor<UserRole>() + verify(userRoleRepository, times(1)).delete(userRoleCaptor.capture()) - assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) - assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.USER) - } + assertThat(userRoleCaptor.firstValue.id).isEqualTo(1L) + assertThat(userRoleCaptor.firstValue.role).isEqualTo(Role.USER) + } - @Test - fun shouldExpireSessionOnDelete() { - val dummySessions = dummySessions() - whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn( - dummySessions - ) + @Test + fun shouldExpireSessionOnDelete() { + val dummySessions = dummySessions() + whenever(sessionRegistry.getAllSessions(any(), any<Boolean>())).thenReturn(dummySessions) - assertThat(dummySessions.filter { it.isExpired }).hasSize(0) + assertThat(dummySessions.filter { it.isExpired }).hasSize(0) - userRoleService.deleteUserRole(1) + userRoleService.deleteUserRole(1) - verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>()) + verify(sessionRegistry, times(1)).getAllSessions(any<OidcUser>(), any<Boolean>()) - assertThat(dummySessions.filter { it.isExpired }).hasSize(2) - } + assertThat(dummySessions.filter { it.isExpired }).hasSize(2) } + } - @Nested - inner class WithoutExistingUserRole { - - @BeforeEach - fun setup() { - doAnswer { _ -> - Optional.empty<UserRole>() - }.whenever(userRoleRepository).findById(any<Long>()) - } + @Nested + inner class WithoutExistingUserRole { - @Test - fun shouldNotUpdateUserRole() { - userRoleService.updateUserRole(1, Role.ADMIN) + @BeforeEach + fun setup() { + doAnswer { _ -> Optional.empty<UserRole>() } + .whenever(userRoleRepository) + .findById(any<Long>()) + } - verify(userRoleRepository, never()).save(any<UserRole>()) - } + @Test + fun shouldNotUpdateUserRole() { + userRoleService.updateUserRole(1, Role.ADMIN) - @Test - fun shouldNotExpireSessionOnUpdate() { - userRoleService.updateUserRole(1, Role.ADMIN) + verify(userRoleRepository, never()).save(any<UserRole>()) + } - verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>()) - } + @Test + fun shouldNotExpireSessionOnUpdate() { + userRoleService.updateUserRole(1, Role.ADMIN) - @Test - fun shouldNotDeleteUserRole() { - userRoleService.deleteUserRole(1) + verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>()) + } - verify(userRoleRepository, never()).delete(any<UserRole>()) - } + @Test + fun shouldNotDeleteUserRole() { + userRoleService.deleteUserRole(1) - @Test - fun shouldNotExpireSessionOnDelete() { - userRoleService.deleteUserRole(1) + verify(userRoleRepository, never()).delete(any<UserRole>()) + } - verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>()) - } + @Test + fun shouldNotExpireSessionOnDelete() { + userRoleService.deleteUserRole(1) + verify(sessionRegistry, never()).getAllSessions(any<OidcUser>(), any<Boolean>()) } + } - - companion object { - private fun dummyPrincipal() = DefaultOidcUser( + companion object { + private fun dummyPrincipal() = + DefaultOidcUser( listOf(), OidcIdToken( "anytokenvalue", Instant.now(), Instant.now().plusSeconds(10), - mapOf("sub" to "testsub", "preferred_username" to "patrick.tester") - ) + mapOf("sub" to "testsub", "preferred_username" to "patrick.tester"), + ), ) - private fun dummySessions() = listOf( + private fun dummySessions() = + listOf( SessionInformation( dummyPrincipal(), "SESSIONID1", @@ -196,7 +185,7 @@ class UserRoleServiceTest { dummyPrincipal(), "SESSIONID2", Date.from(Instant.now()), - ) + ), ) - } -}
\ No newline at end of file + } +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt index bbc8b1a..3e54e71 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ConsentProcessorTest.kt @@ -10,6 +10,11 @@ import dev.dnpm.etl.processor.consent.GicsConsentService import dev.pcvolkmer.mv64e.mtb.Mtb import dev.pcvolkmer.mv64e.mtb.MvhSubmissionType import dev.pcvolkmer.mv64e.mtb.Patient +import java.io.IOException +import java.io.InputStream +import java.time.Instant +import java.time.OffsetDateTime +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.CodeableConcept @@ -28,205 +33,194 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.springframework.core.io.ClassPathResource -import java.io.IOException -import java.io.InputStream -import java.time.Instant -import java.time.OffsetDateTime -import java.util.* @ExtendWith(MockitoExtension::class) class ConsentProcessorTest { - private lateinit var appConfigProperties: AppConfigProperties - private lateinit var gicsConsentService: GicsConsentService - private lateinit var objectMapper: ObjectMapper - private lateinit var gIcsConfigProperties: GIcsConfigProperties - private lateinit var fhirContext: FhirContext - private lateinit var consentProcessor: ConsentProcessor - - @BeforeEach - fun setups( - @Mock gicsConsentService: GicsConsentService, - ) { - - this.gIcsConfigProperties = GIcsConfigProperties("https://gics.example.com") - val jacksonConfig = JacksonConfig() - this.objectMapper = jacksonConfig.objectMapper() - this.fhirContext = JacksonConfig.fhirContext() - this.gicsConsentService = gicsConsentService - this.appConfigProperties = AppConfigProperties(emptyList()) - this.consentProcessor = - ConsentProcessor( - appConfigProperties, - gIcsConfigProperties, - objectMapper, - fhirContext, - gicsConsentService - ) + private lateinit var appConfigProperties: AppConfigProperties + private lateinit var gicsConsentService: GicsConsentService + private lateinit var objectMapper: ObjectMapper + private lateinit var gIcsConfigProperties: GIcsConfigProperties + private lateinit var fhirContext: FhirContext + private lateinit var consentProcessor: ConsentProcessor + + @BeforeEach + fun setups( + @Mock gicsConsentService: GicsConsentService, + ) { + + this.gIcsConfigProperties = GIcsConfigProperties("https://gics.example.com") + val jacksonConfig = JacksonConfig() + this.objectMapper = jacksonConfig.objectMapper() + this.fhirContext = JacksonConfig.fhirContext() + this.gicsConsentService = gicsConsentService + this.appConfigProperties = AppConfigProperties(emptyList()) + this.consentProcessor = + ConsentProcessor( + appConfigProperties, + gIcsConfigProperties, + objectMapper, + fhirContext, + gicsConsentService, + ) + } + + @Test + fun consentOk() { + assertThat(consentProcessor.toString()).isNotNull + // prep gICS response + doAnswer { getDummyBroadConsentBundle() } + .whenever(gicsConsentService) + .getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT)) + + doAnswer { Bundle() } + .whenever(gicsConsentService) + .getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E)) + + val inputMtb = + Mtb.builder() + .patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()) + .build() + val checkResult = consentProcessor.consentGatedCheckAndTryEmbedding(inputMtb) + + assertThat(checkResult).isTrue + assertThat(inputMtb.metadata.researchConsents).isNotEmpty + } + + companion object { + fun getDummyGenomDeConsent(): Consent { + val consent = Consent() + consent.id = "consent 1 id" + consent.patient.reference = "Patient/1234-pat1" + + consent.provision.setType(Consent.ConsentProvisionType.fromCode("deny")) + consent.provision.period.start = Date.from(Instant.parse("2025-08-15T00:00:00.00Z")) + consent.provision.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + + val addProvision1 = consent.provision.addProvision() + addProvision1.setType(Consent.ConsentProvisionType.fromCode("permit")) + addProvision1.period.start = Date.from(Instant.parse("2025-08-15T00:00:00.00Z")) + addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision1.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Teilnahme", + "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung", + ) + ) + ) + + val addProvision2 = consent.provision.addProvision() + addProvision2.setType(Consent.ConsentProvisionType.fromCode("deny")) + addProvision2.period.start = Date.from(Instant.parse("2025-08-15T00:00:00.00Z")) + addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) + addProvision2.code.addLast( + CodeableConcept( + Coding( + "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", + "Rekontaktierung", + "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt", + ) + ) + ) + return consent } - - @Test - fun consentOk() { - assertThat(consentProcessor.toString()).isNotNull - // prep gICS response - doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService) - .getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT)) - - doAnswer { Bundle() }.whenever(gicsConsentService) - .getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E)) - - val inputMtb = Mtb.builder() - .patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build() - val checkResult = consentProcessor.consentGatedCheckAndTryEmbedding(inputMtb) - - assertThat(checkResult).isTrue - assertThat(inputMtb.metadata.researchConsents).isNotEmpty + } + + @ParameterizedTest + @CsvSource( + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,PERMIT,expect permit", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,PERMIT,expect permit date is exactly on start", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2055-08-15T00:00:00+02:00,PERMIT,expect permit date is exactly on end", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2021-08-15T00:00:00+02:00,NULL,date is before start", + "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2060-08-15T00:00:00+02:00,NULL,date is after end", + "2.16.840.1.113883.3.1937.777.24.5.3.27,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,DENY,provision is denied", + "unknownCode,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,NULL,code does not exist - therefore expect NULL", + "2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-08-15T00:00:00+02:00,NULL,system not found - therefore expect NULL", + ) + fun getProvisionTypeByPolicyCode( + code: String?, + system: String?, + timeStamp: String, + expected: String?, + desc: String?, + ) { + val testData = getDummyBroadConsentBundle() + + val requestDate = Date.from(OffsetDateTime.parse(timeStamp).toInstant()) + + val result: Consent.ConsentProvisionType = + consentProcessor.getProvisionTypeByPolicyCode(testData, code, system, requestDate) + assertThat(result).isNotNull() + + assertThat(result).`as`(desc).isEqualTo(Consent.ConsentProvisionType.valueOf(expected!!)) + } + + @Test + fun getProvisionTypeOnEmptyConsent() { + val emptyResources = Bundle().addEntry(Bundle.BundleEntryComponent().setResource(Consent())) + + val requestDate = Date.from(OffsetDateTime.parse("2025-08-15T00:00:00+02:00").toInstant()) + + val result: Consent.ConsentProvisionType = + consentProcessor.getProvisionTypeByPolicyCode( + emptyResources, + "anyCode", + "anySystem", + requestDate, + ) + assertThat(result).isNotNull() + + assertThat(result) + .`as`("empty consent resource - expect NULL") + .isEqualTo(Consent.ConsentProvisionType.NULL) + } + + fun getDummyBroadConsentBundle(): Bundle { + val bundle: InputStream? + try { + bundle = ClassPathResource("fake_broadConsent_gics_response_permit.json").getInputStream() + } catch (e: IOException) { + throw RuntimeException(e) } - companion object { - fun getDummyGenomDeConsent(): Consent { - val consent = Consent() - consent.id = "consent 1 id" - consent.patient.reference = "Patient/1234-pat1" - - consent.provision.setType( - Consent.ConsentProvisionType.fromCode( - "deny" - ) - ) - consent.provision.period.start = - Date.from(Instant.parse("2025-08-15T00:00:00.00Z")) - consent.provision.period.end = - Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) - - val addProvision1 = consent.provision.addProvision() - addProvision1.setType(Consent.ConsentProvisionType.fromCode("permit")) - addProvision1.period.start = Date.from(Instant.parse("2025-08-15T00:00:00.00Z")) - addProvision1.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) - addProvision1.code.addLast( - CodeableConcept( - Coding( - "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", - "Teilnahme", - "Teilnahme am Modellvorhaben und Einwilligung zur Genomsequenzierung" - ) - ) - ) - - val addProvision2 = consent.provision.addProvision() - addProvision2.setType(Consent.ConsentProvisionType.fromCode("deny")) - addProvision2.period.start = Date.from(Instant.parse("2025-08-15T00:00:00.00Z")) - addProvision2.period.end = Date.from(Instant.parse("3000-01-01T00:00:00.00Z")) - addProvision2.code.addLast( - CodeableConcept( - Coding( - "https://ths-greifswald.de/fhir/CodeSystem/gics/Policy/GenomDE_MV", - "Rekontaktierung", - "Re-Identifizierung meiner Daten über die Vertrauensstelle beim Robert Koch-Institut und in die erneute Kontaktaufnahme durch meine behandelnde Ärztin oder meinen behandelnden Arzt" - ) - ) - ) - return consent + return FhirContext.forR4().newJsonParser().parseResource<Bundle>(Bundle::class.java, bundle) + } + + @ParameterizedTest + @ValueSource(booleans = [true, false]) + fun mvSubmissionTypeIsSet(isTestSubmission: Boolean) { + appConfigProperties.genomDeTestSubmission = isTestSubmission + val fixture = + ConsentProcessor( + appConfigProperties, + gIcsConfigProperties, + objectMapper, + fhirContext, + gicsConsentService, + ) + + doAnswer { getDummyBroadConsentBundle() } + .whenever(gicsConsentService) + .getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT)) + + doAnswer { + Bundle().addEntry(Bundle.BundleEntryComponent().setResource(getDummyGenomDeConsent())) } + .whenever(gicsConsentService) + .getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E)) + + val inputMtb = + Mtb.builder() + .patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()) + .build() + val checkResult = fixture.consentGatedCheckAndTryEmbedding(inputMtb) + assertThat(checkResult).isNotNull + + if (isTestSubmission) assertThat(inputMtb.metadata.type).isEqualTo(MvhSubmissionType.TEST) + else { + assertThat(inputMtb.metadata.type).isEqualTo(MvhSubmissionType.INITIAL) } - - @ParameterizedTest - @CsvSource( - "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,PERMIT,expect permit", - "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,PERMIT,expect permit date is exactly on start", - "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2055-08-15T00:00:00+02:00,PERMIT,expect permit date is exactly on end", - "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2021-08-15T00:00:00+02:00,NULL,date is before start", - "2.16.840.1.113883.3.1937.777.24.5.3.8,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2060-08-15T00:00:00+02:00,NULL,date is after end", - "2.16.840.1.113883.3.1937.777.24.5.3.27,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,DENY,provision is denied", - "unknownCode,urn:oid:2.16.840.1.113883.3.1937.777.24.5.3,2025-08-15T00:00:00+02:00,NULL,code does not exist - therefore expect NULL", - "2.16.840.1.113883.3.1937.777.24.5.3.8,XXXX,2025-08-15T00:00:00+02:00,NULL,system not found - therefore expect NULL", - ) - fun getProvisionTypeByPolicyCode( - code: String?, system: String?, timeStamp: String, expected: String?, - desc: String? - ) { - val testData = getDummyBroadConsentBundle() - - val requestDate = Date.from(OffsetDateTime.parse(timeStamp).toInstant()) - - val result: Consent.ConsentProvisionType = - consentProcessor.getProvisionTypeByPolicyCode(testData, code, system, requestDate) - assertThat(result).isNotNull() - - - assertThat(result).`as`(desc) - .isEqualTo(Consent.ConsentProvisionType.valueOf(expected!!)) - } - - @Test - fun getProvisionTypeOnEmptyConsent( - ) { - val emptyResources = Bundle().addEntry(Bundle.BundleEntryComponent().setResource(Consent())) - - val requestDate = Date.from(OffsetDateTime.parse("2025-08-15T00:00:00+02:00").toInstant()) - - val result: Consent.ConsentProvisionType = - consentProcessor.getProvisionTypeByPolicyCode( - emptyResources, - "anyCode", - "anySystem", - requestDate - ) - assertThat(result).isNotNull() - - - assertThat(result).`as`("empty consent resource - expect NULL") - .isEqualTo(Consent.ConsentProvisionType.NULL) - } - - fun getDummyBroadConsentBundle(): Bundle { - val bundle: InputStream? - try { - bundle = ClassPathResource( - "fake_broadConsent_gics_response_permit.json" - ).getInputStream() - } catch (e: IOException) { - throw RuntimeException(e) - } - - return FhirContext.forR4().newJsonParser() - .parseResource<Bundle>(Bundle::class.java, bundle) - } - - @ParameterizedTest - @ValueSource(booleans = [true, false]) - fun mvSubmissionTypeIsSet(isTestSubmission: Boolean) { - appConfigProperties.genomDeTestSubmission = isTestSubmission - val fixture = - ConsentProcessor( - appConfigProperties, - gIcsConfigProperties, - objectMapper, - fhirContext, - gicsConsentService - ) - - doAnswer { getDummyBroadConsentBundle() }.whenever(gicsConsentService) - .getConsent(any(), any(), eq(ConsentDomain.BROAD_CONSENT)) - - doAnswer { - Bundle().addEntry( - Bundle.BundleEntryComponent().setResource(getDummyGenomDeConsent()) - ) - }.whenever(gicsConsentService) - .getConsent(any(), any(), eq(ConsentDomain.MODELLVORHABEN_64E)) - - val inputMtb = Mtb.builder() - .patient(Patient.builder().id("d611d429-5003-11f0-a144-661e92ac9503").build()).build() - val checkResult = fixture.consentGatedCheckAndTryEmbedding(inputMtb) - assertThat(checkResult).isNotNull - - if (isTestSubmission) - assertThat(inputMtb.metadata.type).isEqualTo(MvhSubmissionType.TEST) - else { - assertThat(inputMtb.metadata.type).isEqualTo(MvhSubmissionType.INITIAL) - } - - } - + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ReportServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ReportServiceTest.kt index fc95808..4308fed 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/ReportServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ReportServiceTest.kt @@ -32,17 +32,18 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource class ReportServiceTest { - private lateinit var reportService: ReportService @BeforeEach fun setup() { - this.reportService = ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build())) + this.reportService = + ReportService(ObjectMapper().registerModule(KotlinModule.Builder().build())) } @Test fun shouldParseDataQualityReport() { - val json = """ + val json = + """ { "patient": "4711", "issues": [ @@ -52,7 +53,7 @@ class ReportServiceTest { { "severity": "fatal", "message": "Fatal Message" } ] } - """.trimIndent() + """.trimIndent() val actual = this.reportService.deserialize(json) @@ -71,7 +72,10 @@ class ReportServiceTest { @ParameterizedTest @MethodSource("testData") - fun shouldParseDataQualityReport(json: String, requestStatus: RequestStatus) { + fun shouldParseDataQualityReport( + json: String, + requestStatus: RequestStatus, + ) { val actual = this.reportService.deserialize(json) assertThat(actual.asRequestStatus()).isEqualTo(requestStatus) } @@ -88,74 +92,71 @@ class ReportServiceTest { } companion object { - @JvmStatic - fun testData(): Set<Arguments> { - return setOf( + fun testData(): Set<Arguments> = + setOf( Arguments.of( """ - { - "patient": "4711", - "issues": [ - { "severity": "info", "message": "Info Message" }, - { "severity": "warning", "message": "Warning Message" }, - { "severity": "error", "message": "Error Message" }, - { "severity": "fatal", "message": "Fatal Message" } - ] - } + { + "patient": "4711", + "issues": [ + { "severity": "info", "message": "Info Message" }, + { "severity": "warning", "message": "Warning Message" }, + { "severity": "error", "message": "Error Message" }, + { "severity": "fatal", "message": "Fatal Message" } + ] + } """.trimIndent(), - RequestStatus.ERROR + RequestStatus.ERROR, ), Arguments.of( """ - { - "patient": "4711", - "issues": [ - { "severity": "info", "message": "Info Message" }, - { "severity": "warning", "message": "Warning Message" }, - { "severity": "error", "message": "Error Message" } - ] - } + { + "patient": "4711", + "issues": [ + { "severity": "info", "message": "Info Message" }, + { "severity": "warning", "message": "Warning Message" }, + { "severity": "error", "message": "Error Message" } + ] + } """.trimIndent(), - RequestStatus.ERROR + RequestStatus.ERROR, ), Arguments.of( """ - { - "patient": "4711", - "issues": [ - { "severity": "error", "message": "Error Message" } - { "severity": "info", "message": "Info Message" } - ] - } + { + "patient": "4711", + "issues": [ + { "severity": "error", "message": "Error Message" } + { "severity": "info", "message": "Info Message" } + ] + } """.trimIndent(), - RequestStatus.ERROR + RequestStatus.ERROR, ), Arguments.of( """ - { - "patient": "4711", - "issues": [ - { "severity": "info", "message": "Info Message" }, - { "severity": "warning", "message": "Warning Message" } - ] - } + { + "patient": "4711", + "issues": [ + { "severity": "info", "message": "Info Message" }, + { "severity": "warning", "message": "Warning Message" } + ] + } """.trimIndent(), - RequestStatus.WARNING + RequestStatus.WARNING, ), Arguments.of( """ - { - "patient": "4711", - "issues": [ - { "severity": "info", "message": "Info Message" } - ] - } + { + "patient": "4711", + "issues": [ + { "severity": "info", "message": "Info Message" } + ] + } """.trimIndent(), - RequestStatus.SUCCESS - ) + RequestStatus.SUCCESS, + ), ) - } } - -}
\ 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 0a42b9b..4bd3fc1 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestProcessorTest.kt @@ -35,6 +35,8 @@ import dev.dnpm.etl.processor.output.RestMtbFileSender import dev.dnpm.etl.processor.pseudonym.PseudonymizeService import dev.dnpm.etl.processor.randomRequestId import dev.pcvolkmer.mv64e.mtb.* +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -47,42 +49,40 @@ import org.mockito.kotlin.anyValueClass import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever import org.springframework.context.ApplicationEventPublisher -import java.time.Instant -import java.util.* - @ExtendWith(MockitoExtension::class) class RequestProcessorTest { - private lateinit var pseudonymizeService: PseudonymizeService - private lateinit var transformationService: TransformationService - private lateinit var sender: MtbFileSender - private lateinit var requestService: RequestService - private lateinit var applicationEventPublisher: ApplicationEventPublisher - private lateinit var appConfigProperties: AppConfigProperties - private lateinit var consentProcessor: ConsentProcessor - private lateinit var requestProcessor: RequestProcessor - - @BeforeEach - fun setup( - @Mock pseudonymizeService: PseudonymizeService, - @Mock transformationService: TransformationService, - @Mock sender: RestMtbFileSender, - @Mock requestService: RequestService, - @Mock applicationEventPublisher: ApplicationEventPublisher, - @Mock consentProcessor: ConsentProcessor - ) { - this.pseudonymizeService = pseudonymizeService - this.transformationService = transformationService - this.sender = sender - this.requestService = requestService - this.applicationEventPublisher = applicationEventPublisher - this.appConfigProperties = AppConfigProperties() - this.consentProcessor = consentProcessor - - val objectMapper = ObjectMapper() - - requestProcessor = RequestProcessor( + private lateinit var pseudonymizeService: PseudonymizeService + private lateinit var transformationService: TransformationService + private lateinit var sender: MtbFileSender + private lateinit var requestService: RequestService + private lateinit var applicationEventPublisher: ApplicationEventPublisher + private lateinit var appConfigProperties: AppConfigProperties + private lateinit var consentProcessor: ConsentProcessor + private lateinit var requestProcessor: RequestProcessor + + @BeforeEach + fun setup( + @Mock pseudonymizeService: PseudonymizeService, + @Mock transformationService: TransformationService, + @Mock sender: RestMtbFileSender, + @Mock requestService: RequestService, + @Mock applicationEventPublisher: ApplicationEventPublisher, + @Mock consentProcessor: ConsentProcessor, + ) { + this.pseudonymizeService = pseudonymizeService + this.transformationService = transformationService + this.sender = sender + this.requestService = requestService + this.applicationEventPublisher = applicationEventPublisher + this.appConfigProperties = AppConfigProperties() + this.consentProcessor = consentProcessor + + val objectMapper = ObjectMapper() + + requestProcessor = + RequestProcessor( pseudonymizeService, transformationService, sender, @@ -90,224 +90,228 @@ class RequestProcessorTest { objectMapper, applicationEventPublisher, appConfigProperties, - consentProcessor + consentProcessor, ) - } - - @Test - fun testShouldSendMtbFileDuplicationAndSaveUnknownRequestStatusAtFirst() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("6vkiti5bk6ikwifpajpt7cygmd3dvw54d6lwfhzlynb3pqtzferq"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS, - Instant.parse("2023-08-08T02:00:00Z") - ) - }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass()) - - doAnswer { - false - }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass()) - - doAnswer { - it.arguments[0] as String - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - it.arguments[0] - }.whenever(transformationService).transform(any<Mtb>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .id("123") - .build() - ) + } + + @Test + fun testShouldSendMtbFileDuplicationAndSaveUnknownRequestStatusAtFirst() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("6vkiti5bk6ikwifpajpt7cygmd3dvw54d6lwfhzlynb3pqtzferq"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + Instant.parse("2023-08-08T02:00:00Z"), + ) + } + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) .episodesOfCare( listOf( MtbEpisodeOfCare.builder() .id("1") .patient(Reference.builder().id("123").build()) - .period(PeriodDate.builder().start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))).build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))) + .build() + ) .build() ) ) .build() - this.requestProcessor.processMtbFile(mtbFile) - - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) - } - - @Test - fun testShouldDetectMtbFileDuplicationAndSendDuplicationEvent() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("4gcjwtjjtcczybsljxepdfpkaeusvd7g3vogfqpmphyffyzfx7dq"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS, - Instant.parse("2023-08-08T02:00:00Z") - ) - }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass()) - - doAnswer { - false - }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass()) - - doAnswer { - it.arguments[0] as String - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - it.arguments[0] - }.whenever(transformationService).transform(any<Mtb>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .id("123") - .build() - ) + this.requestProcessor.processMtbFile(mtbFile) + + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + } + + @Test + fun testShouldDetectMtbFileDuplicationAndSendDuplicationEvent() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("4gcjwtjjtcczybsljxepdfpkaeusvd7g3vogfqpmphyffyzfx7dq"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + Instant.parse("2023-08-08T02:00:00Z"), + ) + } + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) .episodesOfCare( listOf( MtbEpisodeOfCare.builder() .id("1") .patient(Reference.builder().id("123").build()) - .period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) .build() ) ) .build() - this.requestProcessor.processMtbFile(mtbFile) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.DUPLICATION) - } - - @Test - fun testShouldSendMtbFileAndSendSuccessEvent() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("different"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS, - Instant.parse("2023-08-08T02:00:00Z") - ) - }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass()) - - doAnswer { - false - }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass()) - - doAnswer { - MtbFileSender.Response(status = RequestStatus.SUCCESS) - }.whenever(sender).send(any<DnpmV2MtbFileRequest>()) - - doAnswer { - it.arguments[0] as String - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - it.arguments[0] - }.whenever(transformationService).transform(any<Mtb>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .id("123") - .build() - ) + this.requestProcessor.processMtbFile(mtbFile) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.DUPLICATION) + } + + @Test + fun testShouldSendMtbFileAndSendSuccessEvent() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("different"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + Instant.parse("2023-08-08T02:00:00Z"), + ) + } + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) .episodesOfCare( listOf( MtbEpisodeOfCare.builder() .id("1") .patient(Reference.builder().id("123").build()) - .period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) .build() ) ) .build() - this.requestProcessor.processMtbFile(mtbFile) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } - - @Test - fun testShouldSendMtbFileAndSendErrorEvent() { - doAnswer { - Request( - 1L, - randomRequestId(), - PatientPseudonym("TEST_12345678901"), - PatientId("P1"), - Fingerprint("different"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS, - Instant.parse("2023-08-08T02:00:00Z") - ) - }.whenever(requestService).lastMtbFileRequestForPatientPseudonym(anyValueClass()) - - doAnswer { - false - }.whenever(requestService).isLastRequestWithKnownStatusDeletion(anyValueClass()) - - doAnswer { - MtbFileSender.Response(status = RequestStatus.ERROR) - }.whenever(sender).send(any<DnpmV2MtbFileRequest>()) - - doAnswer { - it.arguments[0] as String - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - it.arguments[0] - }.whenever(transformationService).transform(any<Mtb>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .id("123") - .build() - ) + this.requestProcessor.processMtbFile(mtbFile) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } + + @Test + fun testShouldSendMtbFileAndSendErrorEvent() { + doAnswer { + Request( + 1L, + randomRequestId(), + PatientPseudonym("TEST_12345678901"), + PatientId("P1"), + Fingerprint("different"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + Instant.parse("2023-08-08T02:00:00Z"), + ) + } + .whenever(requestService) + .lastMtbFileRequestForPatientPseudonym(anyValueClass()) + + doAnswer { false } + .whenever(requestService) + .isLastRequestWithKnownStatusDeletion(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.ERROR) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) .metadata( - MvhMetadata - .builder() + MvhMetadata.builder() .modelProjectConsent( - ModelProjectConsent - .builder() + ModelProjectConsent.builder() .provisions( - listOf(Provision.builder().type(ConsentProvision.PERMIT).purpose(ModelProjectConsentPurpose.SEQUENCING).build()) - ).build() + listOf( + Provision.builder() + .type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .build() + ) + ) + .build() ) .build() ) @@ -316,143 +320,139 @@ class RequestProcessorTest { MtbEpisodeOfCare.builder() .id("1") .patient(Reference.builder().id("123").build()) - .period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) .build() ) ) .build() - this.requestProcessor.processMtbFile(mtbFile) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) - } - - @Test - fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() { - doAnswer { - "PSEUDONYM" - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - MtbFileSender.Response(status = RequestStatus.UNKNOWN) - }.whenever(sender).send(any<DeleteRequest>()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE - ) - - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) - } - - @Test - fun testShouldSendDeleteRequestAndSendSuccessEvent() { - doAnswer { - "PSEUDONYM" - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - MtbFileSender.Response(status = RequestStatus.SUCCESS) - }.whenever(sender).send(any<DeleteRequest>()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE - ) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } - - @Test - fun testShouldSendDeleteRequestAndSendErrorEvent() { - doAnswer { - "PSEUDONYM" - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - MtbFileSender.Response(status = RequestStatus.ERROR) - }.whenever(sender).send(any<DeleteRequest>()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE - ) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) - } - - @Test - fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() { - doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - this.requestProcessor.processDeletion( - TEST_PATIENT_ID, - isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE - ) - - val requestCaptor = argumentCaptor<Request>() - verify(requestService, times(1)).save(requestCaptor.capture()) - assertThat(requestCaptor.firstValue).isNotNull - assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) - } - - @Test - fun testShouldNotDetectMtbFileDuplicationIfDuplicationNotConfigured() { - this.appConfigProperties.duplicationDetection = false - - doAnswer { - it.arguments[0] as String - }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) - - doAnswer { - it.arguments[0] - }.whenever(transformationService).transform(any<Mtb>()) - - doAnswer { - MtbFileSender.Response(status = RequestStatus.SUCCESS) - }.whenever(sender).send(any<DnpmV2MtbFileRequest>()) - - whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) - - val mtbFile = Mtb.builder() - .patient( - Patient.builder() - .id("123") - .build() - ) + this.requestProcessor.processMtbFile(mtbFile) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) + } + + @Test + fun testShouldSendDeleteRequestAndSaveUnknownRequestStatusAtFirst() { + doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.UNKNOWN) } + .whenever(sender) + .send(any<DeleteRequest>()) + + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, + ) + + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.UNKNOWN) + } + + @Test + fun testShouldSendDeleteRequestAndSendSuccessEvent() { + doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DeleteRequest>()) + + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, + ) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } + + @Test + fun testShouldSendDeleteRequestAndSendErrorEvent() { + doAnswer { "PSEUDONYM" }.whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.ERROR) } + .whenever(sender) + .send(any<DeleteRequest>()) + + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, + ) + + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) + } + + @Test + fun testShouldSendDeleteRequestWithPseudonymErrorAndSaveErrorRequestStatus() { + doThrow(RuntimeException()).whenever(pseudonymizeService).patientPseudonym(anyValueClass()) + + this.requestProcessor.processDeletion( + TEST_PATIENT_ID, + isConsented = TtpConsentStatus.UNKNOWN_CHECK_FILE, + ) + + val requestCaptor = argumentCaptor<Request>() + verify(requestService, times(1)).save(requestCaptor.capture()) + assertThat(requestCaptor.firstValue).isNotNull + assertThat(requestCaptor.firstValue.status).isEqualTo(RequestStatus.ERROR) + } + + @Test + fun testShouldNotDetectMtbFileDuplicationIfDuplicationNotConfigured() { + this.appConfigProperties.duplicationDetection = false + + doAnswer { it.arguments[0] as String } + .whenever(pseudonymizeService) + .patientPseudonym(anyValueClass()) + + doAnswer { it.arguments[0] }.whenever(transformationService).transform(any<Mtb>()) + + doAnswer { MtbFileSender.Response(status = RequestStatus.SUCCESS) } + .whenever(sender) + .send(any<DnpmV2MtbFileRequest>()) + + whenever(consentProcessor.consentGatedCheckAndTryEmbedding(any())).thenReturn(true) + + val mtbFile = + Mtb.builder() + .patient(Patient.builder().id("123").build()) .episodesOfCare( listOf( MtbEpisodeOfCare.builder() .id("1") .patient(Reference.builder().id("123").build()) - .period(PeriodDate.builder().start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))).build()) + .period( + PeriodDate.builder() + .start(Date.from(Instant.parse("2021-01-01T00:00:00.00Z"))) + .build() + ) .build() ) ) .build() - this.requestProcessor.processMtbFile(mtbFile) - - val eventCaptor = argumentCaptor<ResponseEvent>() - verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) - assertThat(eventCaptor.firstValue).isNotNull - assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) - } + this.requestProcessor.processMtbFile(mtbFile) - companion object { - val TEST_PATIENT_ID = PatientId("TEST_12345678901") - } + val eventCaptor = argumentCaptor<ResponseEvent>() + verify(applicationEventPublisher, times(1)).publishEvent(eventCaptor.capture()) + assertThat(eventCaptor.firstValue).isNotNull + assertThat(eventCaptor.firstValue.status).isEqualTo(RequestStatus.SUCCESS) + } + companion object { + val TEST_PATIENT_ID = PatientId("TEST_12345678901") + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt index c0e4400..bc0286c 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/RequestServiceTest.kt @@ -24,6 +24,7 @@ import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestRepository import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType +import java.time.Instant import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -33,37 +34,37 @@ import org.mockito.Mockito.* import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.anyValueClass import org.mockito.kotlin.whenever -import java.time.Instant @ExtendWith(MockitoExtension::class) class RequestServiceTest { - private lateinit var requestRepository: RequestRepository - - private lateinit var requestService: RequestService - - private fun anyRequest() = any(Request::class.java) ?: Request( - 0L, - randomRequestId(), - PatientPseudonym("TEST_dummy"), - PatientId("PX"), - Fingerprint("dummy"), - RequestType.MTB_FILE, - RequestStatus.SUCCESS, - Instant.parse("2023-08-08T02:00:00Z") - ) - - @BeforeEach - fun setup( - @Mock requestRepository: RequestRepository - ) { - this.requestRepository = requestRepository - this.requestService = RequestService(requestRepository) - } - - @Test - fun shouldIndicateLastRequestIsDeleteRequest() { - val requests = listOf( + private lateinit var requestRepository: RequestRepository + + private lateinit var requestService: RequestService + + private fun anyRequest() = + any(Request::class.java) + ?: Request( + 0L, + randomRequestId(), + PatientPseudonym("TEST_dummy"), + PatientId("PX"), + Fingerprint("dummy"), + RequestType.MTB_FILE, + RequestStatus.SUCCESS, + Instant.parse("2023-08-08T02:00:00Z"), + ) + + @BeforeEach + fun setup(@Mock requestRepository: RequestRepository) { + this.requestRepository = requestRepository + this.requestService = RequestService(requestRepository) + } + + @Test + fun shouldIndicateLastRequestIsDeleteRequest() { + val requests = + listOf( Request( 1L, randomRequestId(), @@ -72,7 +73,7 @@ class RequestServiceTest { Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, RequestStatus.WARNING, - Instant.parse("2023-07-07T00:00:00Z") + Instant.parse("2023-07-07T00:00:00Z"), ), Request( 2L, @@ -82,7 +83,7 @@ class RequestServiceTest { Fingerprint("0123456789abcdefd"), RequestType.DELETE, RequestStatus.WARNING, - Instant.parse("2023-07-07T02:00:00Z") + Instant.parse("2023-07-07T02:00:00Z"), ), Request( 3L, @@ -92,18 +93,19 @@ class RequestServiceTest { Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, RequestStatus.UNKNOWN, - Instant.parse("2023-08-11T00:00:00Z") - ) + Instant.parse("2023-08-11T00:00:00Z"), + ), ) - val actual = RequestService.isLastRequestWithKnownStatusDeletion(requests) + val actual = RequestService.isLastRequestWithKnownStatusDeletion(requests) - assertThat(actual).isTrue() - } + assertThat(actual).isTrue() + } - @Test - fun shouldIndicateLastRequestIsNotDeleteRequest() { - val requests = listOf( + @Test + fun shouldIndicateLastRequestIsNotDeleteRequest() { + val requests = + listOf( Request( 1L, randomRequestId(), @@ -112,7 +114,7 @@ class RequestServiceTest { Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, RequestStatus.WARNING, - Instant.parse("2023-07-07T00:00:00Z") + Instant.parse("2023-07-07T00:00:00Z"), ), Request( 2L, @@ -122,7 +124,7 @@ class RequestServiceTest { Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, RequestStatus.WARNING, - Instant.parse("2023-07-07T02:00:00Z") + Instant.parse("2023-07-07T02:00:00Z"), ), Request( 3L, @@ -132,18 +134,19 @@ class RequestServiceTest { Fingerprint("0123456789abcdef1"), RequestType.MTB_FILE, RequestStatus.UNKNOWN, - Instant.parse("2023-08-11T00:00:00Z") - ) + Instant.parse("2023-08-11T00:00:00Z"), + ), ) - val actual = RequestService.isLastRequestWithKnownStatusDeletion(requests) + val actual = RequestService.isLastRequestWithKnownStatusDeletion(requests) - assertThat(actual).isFalse() - } + assertThat(actual).isFalse() + } - @Test - fun shouldReturnPatientsLastRequest() { - val requests = listOf( + @Test + fun shouldReturnPatientsLastRequest() { + val requests = + listOf( Request( 1L, randomRequestId(), @@ -152,7 +155,7 @@ class RequestServiceTest { Fingerprint("0123456789abcdef1"), RequestType.DELETE, RequestStatus.SUCCESS, - Instant.parse("2023-07-07T02:00:00Z") + Instant.parse("2023-07-07T02:00:00Z"), ), Request( 1L, @@ -162,66 +165,71 @@ class RequestServiceTest { Fingerprint("0123456789abcdef2"), RequestType.MTB_FILE, RequestStatus.WARNING, - Instant.parse("2023-08-08T00:00:00Z") - ) + Instant.parse("2023-08-08T00:00:00Z"), + ), ) - val actual = RequestService.lastMtbFileRequestForPatientPseudonym(requests) + val actual = RequestService.lastMtbFileRequestForPatientPseudonym(requests) - assertThat(actual).isInstanceOf(Request::class.java) - assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef2")) - } + assertThat(actual).isInstanceOf(Request::class.java) + assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef2")) + } - @Test - fun shouldReturnNullIfNoRequests() { - val requests = listOf<Request>() + @Test + fun shouldReturnNullIfNoRequests() { + val requests = listOf<Request>() - val actual = RequestService.lastMtbFileRequestForPatientPseudonym(requests) + val actual = RequestService.lastMtbFileRequestForPatientPseudonym(requests) - assertThat(actual).isNull() - } + assertThat(actual).isNull() + } - @Test - fun saveShouldSaveRequestUsingRepository() { - doAnswer { - val obj = it.arguments[0] as Request - obj.copy(id = 1L) - }.whenever(requestRepository).save(anyRequest()) + @Test + fun saveShouldSaveRequestUsingRepository() { + doAnswer { + val obj = it.arguments[0] as Request + obj.copy(id = 1L) + } + .whenever(requestRepository) + .save(anyRequest()) - val request = Request( + val request = + Request( randomRequestId(), PatientPseudonym("TEST_12345678901"), PatientId("P1"), Fingerprint("0123456789abcdef1"), RequestType.DELETE, RequestStatus.SUCCESS, - Instant.parse("2023-07-07T02:00:00Z") + Instant.parse("2023-07-07T02:00:00Z"), ) - requestService.save(request) - - verify(requestRepository, times(1)).save(anyRequest()) - } + requestService.save(request) - @Test - fun allRequestsByPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() { - requestService.allRequestsByPatientPseudonym(PatientPseudonym("TEST_12345678901")) + verify(requestRepository, times(1)).save(anyRequest()) + } - verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass()) - } + @Test + fun allRequestsByPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() { + requestService.allRequestsByPatientPseudonym(PatientPseudonym("TEST_12345678901")) - @Test - fun lastMtbFileRequestForPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() { - requestService.lastMtbFileRequestForPatientPseudonym(PatientPseudonym("TEST_12345678901")) + verify(requestRepository, times(1)) + .findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass()) + } - verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass()) - } + @Test + fun lastMtbFileRequestForPatientPseudonymShouldRequestAllRequestsForPatientPseudonym() { + requestService.lastMtbFileRequestForPatientPseudonym(PatientPseudonym("TEST_12345678901")) - @Test - fun isLastRequestDeletionShouldRequestAllRequestsForPatientPseudonym() { - requestService.isLastRequestWithKnownStatusDeletion(PatientPseudonym("TEST_12345678901")) + verify(requestRepository, times(1)) + .findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass()) + } - verify(requestRepository, times(1)).findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass()) - } + @Test + fun isLastRequestDeletionShouldRequestAllRequestsForPatientPseudonym() { + requestService.isLastRequestWithKnownStatusDeletion(PatientPseudonym("TEST_12345678901")) -}
\ No newline at end of file + verify(requestRepository, times(1)) + .findAllByPatientPseudonymOrderByProcessedAtDesc(anyValueClass()) + } +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt index 465d8b8..16a5791 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/ResponseProcessorTest.kt @@ -23,6 +23,8 @@ import dev.dnpm.etl.processor.* import dev.dnpm.etl.processor.monitoring.Request import dev.dnpm.etl.processor.monitoring.RequestStatus import dev.dnpm.etl.processor.monitoring.RequestType +import java.time.Instant +import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -33,105 +35,92 @@ import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.* import reactor.core.publisher.Sinks -import java.time.Instant -import java.util.* @ExtendWith(MockitoExtension::class) class ResponseProcessorTest { - private lateinit var requestService: RequestService - private lateinit var statisticsUpdateProducer: Sinks.Many<Any> - - private lateinit var responseProcessor: ResponseProcessor - - private val testRequest = Request( - 1L, - RequestId("TestID1234"), - PatientPseudonym("PSEUDONYM-A"), - PatientId("1"), - Fingerprint("dummyfingerprint"), - RequestType.MTB_FILE, - RequestStatus.UNKNOWN - ) - - @BeforeEach - fun setup( - @Mock requestService: RequestService, - @Mock statisticsUpdateProducer: Sinks.Many<Any> - ) { - this.requestService = requestService - this.statisticsUpdateProducer = statisticsUpdateProducer - - this.responseProcessor = ResponseProcessor(requestService, statisticsUpdateProducer) - } + private lateinit var requestService: RequestService + private lateinit var statisticsUpdateProducer: Sinks.Many<Any> - @Test - fun shouldNotSaveStatusForUnknownRequest() { - doAnswer { - Optional.empty<Request>() - }.whenever(requestService).findByUuid(anyValueClass()) + private lateinit var responseProcessor: ResponseProcessor - val event = ResponseEvent( - RequestId("TestID1234"), - Instant.parse("2023-09-09T00:00:00Z"), - RequestStatus.SUCCESS - ) + private val testRequest = + Request( + 1L, + RequestId("TestID1234"), + PatientPseudonym("PSEUDONYM-A"), + PatientId("1"), + Fingerprint("dummyfingerprint"), + RequestType.MTB_FILE, + RequestStatus.UNKNOWN, + ) - this.responseProcessor.handleResponseEvent(event) + @BeforeEach + fun setup(@Mock requestService: RequestService, @Mock statisticsUpdateProducer: Sinks.Many<Any>) { + this.requestService = requestService + this.statisticsUpdateProducer = statisticsUpdateProducer - verify(requestService, never()).save(any()) - } + this.responseProcessor = ResponseProcessor(requestService, statisticsUpdateProducer) + } - @Test - fun shouldNotSaveStatusWithUnknownState() { - doAnswer { - Optional.of(testRequest) - }.whenever(requestService).findByUuid(anyValueClass()) + @Test + fun shouldNotSaveStatusForUnknownRequest() { + doAnswer { Optional.empty<Request>() }.whenever(requestService).findByUuid(anyValueClass()) - val event = ResponseEvent( + val event = + ResponseEvent( RequestId("TestID1234"), Instant.parse("2023-09-09T00:00:00Z"), - RequestStatus.UNKNOWN + RequestStatus.SUCCESS, ) - this.responseProcessor.handleResponseEvent(event) + this.responseProcessor.handleResponseEvent(event) - verify(requestService, never()).save(any<Request>()) - } + verify(requestService, never()).save(any()) + } - @ParameterizedTest - @MethodSource("requestStatusSource") - fun shouldSaveStatusForKnownRequest(requestStatus: RequestStatus) { - doAnswer { - Optional.of(testRequest) - }.whenever(requestService).findByUuid(anyValueClass()) + @Test + fun shouldNotSaveStatusWithUnknownState() { + doAnswer { Optional.of(testRequest) }.whenever(requestService).findByUuid(anyValueClass()) - val event = ResponseEvent( + val event = + ResponseEvent( RequestId("TestID1234"), Instant.parse("2023-09-09T00:00:00Z"), - requestStatus + RequestStatus.UNKNOWN, ) - this.responseProcessor.handleResponseEvent(event) + this.responseProcessor.handleResponseEvent(event) - val captor = argumentCaptor<Request>() - verify(requestService, times(1)).save(captor.capture()) - assertThat(captor.firstValue).isNotNull - assertThat(captor.firstValue.status).isEqualTo(requestStatus) - } + verify(requestService, never()).save(any<Request>()) + } - companion object { + @ParameterizedTest + @MethodSource("requestStatusSource") + fun shouldSaveStatusForKnownRequest(requestStatus: RequestStatus) { + doAnswer { Optional.of(testRequest) }.whenever(requestService).findByUuid(anyValueClass()) - @JvmStatic - fun requestStatusSource(): Set<RequestStatus> { - return setOf( - RequestStatus.SUCCESS, - RequestStatus.WARNING, - RequestStatus.ERROR, - RequestStatus.DUPLICATION - ) - } + val event = + ResponseEvent(RequestId("TestID1234"), Instant.parse("2023-09-09T00:00:00Z"), requestStatus) - } + this.responseProcessor.handleResponseEvent(event) -}
\ No newline at end of file + val captor = argumentCaptor<Request>() + verify(requestService, times(1)).save(captor.capture()) + assertThat(captor.firstValue).isNotNull + assertThat(captor.firstValue.status).isEqualTo(requestStatus) + } + + companion object { + + @JvmStatic + fun requestStatusSource(): Set<RequestStatus> { + return setOf( + RequestStatus.SUCCESS, + RequestStatus.WARNING, + RequestStatus.ERROR, + RequestStatus.DUPLICATION, + ) + } + } +} diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt index ba9d23f..c6438e8 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/TransformationServiceTest.kt @@ -21,106 +21,139 @@ package dev.dnpm.etl.processor.services import dev.dnpm.etl.processor.config.JacksonConfig import dev.pcvolkmer.mv64e.mtb.* +import java.time.Instant +import java.util.Date import org.assertj.core.api.Assertions.assertThat +import org.hl7.fhir.instance.model.api.IBaseResource import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.hl7.fhir.instance.model.api.IBaseResource -import java.time.Instant -import java.util.Date class TransformationServiceTest { - private lateinit var service: TransformationService + private lateinit var service: TransformationService - @BeforeEach - fun setup() { - this.service = TransformationService( - JacksonConfig().objectMapper(), listOf( + @BeforeEach + fun setup() { + this.service = + TransformationService( + JacksonConfig().objectMapper(), + listOf( Transformation.of("diagnoses[*].code.version") from "2013" to "2014", - ) + ), ) - } - - @Test - fun shouldTransformMtbFile() { - val mtbFile = Mtb.builder().diagnoses( - listOf( - MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.9").version("2013").build()).build() + } + + @Test + fun shouldTransformMtbFile() { + val mtbFile = + Mtb.builder() + .diagnoses( + listOf( + MtbDiagnosis.builder() + .id("1234") + .code(Coding.builder().code("F79.9").version("2013").build()) + .build() + ) ) - ).build() - - val actual = this.service.transform(mtbFile) - - assertThat(actual).isNotNull - assertThat(actual.diagnoses[0].code.version).isEqualTo("2014") - } - - @Test - fun shouldOnlyTransformGivenValues() { - val mtbFile = Mtb.builder().diagnoses( - listOf( - MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.9").version("2013").build()).build(), - MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.8").version("2019").build()).build() + .build() + + val actual = this.service.transform(mtbFile) + + assertThat(actual).isNotNull + assertThat(actual.diagnoses[0].code.version).isEqualTo("2014") + } + + @Test + fun shouldOnlyTransformGivenValues() { + val mtbFile = + Mtb.builder() + .diagnoses( + listOf( + MtbDiagnosis.builder() + .id("1234") + .code(Coding.builder().code("F79.9").version("2013").build()) + .build(), + MtbDiagnosis.builder() + .id("1234") + .code(Coding.builder().code("F79.8").version("2019").build()) + .build(), + ) ) - ).build() - - val actual = this.service.transform(mtbFile) + .build() + + val actual = this.service.transform(mtbFile) + + assertThat(actual).isNotNull + assertThat(actual.diagnoses[0].code.code).isEqualTo("F79.9") + assertThat(actual.diagnoses[0].code.version).isEqualTo("2014") + assertThat(actual.diagnoses[1].code.code).isEqualTo("F79.8") + assertThat(actual.diagnoses[1].code.version).isEqualTo("2019") + } + + @Test + fun shouldTransformConsentValues() { + val mtbFile = + Mtb.builder() + .diagnoses( + listOf( + MtbDiagnosis.builder() + .id("1234") + .code(Coding.builder().code("F79.9").version("2013").build()) + .build(), + MtbDiagnosis.builder() + .id("1234") + .code(Coding.builder().code("F79.8").version("2019").build()) + .build(), + ) + ) + .build() + + val actual = this.service.transform(mtbFile) + + assertThat(actual).isNotNull + assertThat(actual.diagnoses[0].code.code).isEqualTo("F79.9") + assertThat(actual.diagnoses[0].code.version).isEqualTo("2014") + assertThat(actual.diagnoses[1].code.code).isEqualTo("F79.8") + assertThat(actual.diagnoses[1].code.version).isEqualTo("2019") + } + + @Test + fun shouldTransformConsent() { + val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build() + + assertThat(mvhMetadata).isNotNull + mvhMetadata.modelProjectConsent = + ModelProjectConsent.builder() + .date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))) + .version("1") + .provisions( + listOf( + Provision.builder() + .type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.SEQUENCING) + .date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))) + .build(), + Provision.builder() + .type(ConsentProvision.PERMIT) + .purpose(ModelProjectConsentPurpose.REIDENTIFICATION) + .date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))) + .build(), + Provision.builder() + .type(ConsentProvision.DENY) + .purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION) + .date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))) + .build(), + ) + ) + .build() + val consent = ConsentProcessorTest.getDummyGenomDeConsent() - assertThat(actual).isNotNull - assertThat(actual.diagnoses[0].code.code).isEqualTo("F79.9") - assertThat(actual.diagnoses[0].code.version).isEqualTo("2014") - assertThat(actual.diagnoses[1].code.code).isEqualTo("F79.8") - assertThat(actual.diagnoses[1].code.version).isEqualTo("2019") - } + mvhMetadata.researchConsents = mutableListOf() + mvhMetadata.researchConsents.add(mapOf(consent.id to consent as IBaseResource)) - @Test - fun shouldTransformConsentValues() { - val mtbFile = Mtb.builder().diagnoses( - listOf( - MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.9").version("2013").build()).build(), - MtbDiagnosis.builder().id("1234").code(Coding.builder().code("F79.8").version("2019").build()).build() - ) - ).build() - - val actual = this.service.transform(mtbFile) - - assertThat(actual).isNotNull - assertThat(actual.diagnoses[0].code.code).isEqualTo("F79.9") - assertThat(actual.diagnoses[0].code.version).isEqualTo("2014") - assertThat(actual.diagnoses[1].code.code).isEqualTo("F79.8") - assertThat(actual.diagnoses[1].code.version).isEqualTo("2019") - } - - @Test - fun shouldTransformConsent() { - val mvhMetadata = MvhMetadata.builder().transferTan("transfertan12345").build() - - assertThat(mvhMetadata).isNotNull - mvhMetadata.modelProjectConsent = - ModelProjectConsent.builder().date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))) - .version("1").provisions( - listOf( - Provision.builder().type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.SEQUENCING) - .date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))).build(), - Provision.builder().type(ConsentProvision.PERMIT) - .purpose(ModelProjectConsentPurpose.REIDENTIFICATION) - .date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))).build(), - Provision.builder().type(ConsentProvision.DENY) - .purpose(ModelProjectConsentPurpose.CASE_IDENTIFICATION) - .date(Date.from(Instant.parse("2025-08-15T00:00:00.00Z"))).build() - ) - ).build() - val consent = ConsentProcessorTest.getDummyGenomDeConsent() - - mvhMetadata.researchConsents = mutableListOf() - mvhMetadata.researchConsents.add(mapOf(consent.id to consent as IBaseResource)) - - val mtbFile = Mtb.builder().metadata(mvhMetadata).build() - - val transformed = service.transform(mtbFile) - assertThat(transformed.metadata.modelProjectConsent.date).isNotNull - - } + val mtbFile = Mtb.builder().metadata(mvhMetadata).build() + val transformed = service.transform(mtbFile) + assertThat(transformed.metadata.modelProjectConsent.date).isNotNull + } } diff --git a/src/test/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessorTest.kt b/src/test/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessorTest.kt index 95bf41b..8d5024a 100644 --- a/src/test/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessorTest.kt +++ b/src/test/kotlin/dev/dnpm/etl/processor/services/kafka/KafkaResponseProcessorTest.kt @@ -39,7 +39,6 @@ import org.springframework.http.HttpStatus @ExtendWith(MockitoExtension::class) class KafkaResponseProcessorTest { - private lateinit var eventPublisher: ApplicationEventPublisher private lateinit var objectMapper: ObjectMapper @@ -48,9 +47,9 @@ class KafkaResponseProcessorTest { private fun createKafkaRecord( requestId: String, statusCode: Int = 200, - statusBody: Map<String, Any>? = mapOf() - ): ConsumerRecord<String, String> { - return ConsumerRecord<String, String>( + statusBody: Map<String, Any>? = mapOf(), + ): ConsumerRecord<String, String> = + ConsumerRecord<String, String>( "test-topic", 0, 0, @@ -58,14 +57,15 @@ class KafkaResponseProcessorTest { if (statusBody == null) { "" } else { - this.objectMapper.writeValueAsString(KafkaResponseProcessor.ResponseBody(requestId, statusCode, statusBody)) - } + this.objectMapper.writeValueAsString( + KafkaResponseProcessor.ResponseBody(requestId, statusCode, statusBody), + ) + }, ) - } @BeforeEach fun setup( - @Mock eventPublisher: ApplicationEventPublisher + @Mock eventPublisher: ApplicationEventPublisher, ) { this.eventPublisher = eventPublisher this.objectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build()) @@ -75,18 +75,19 @@ class KafkaResponseProcessorTest { @Test fun shouldNotProcessRecordsWithoutRequestIdInBody() { - val record = ConsumerRecord<String, String>( - "test-topic", - 0, - 0, - null, - """ + val record = + ConsumerRecord<String, String>( + "test-topic", + 0, + 0, + null, + """ { "statusCode": 200, "statusBody": {} } - """.trimIndent() - ) + """.trimIndent(), + ) this.kafkaResponseProcessor.onMessage(record) @@ -95,19 +96,20 @@ class KafkaResponseProcessorTest { @Test fun shouldProcessRecordsWithAliasNames() { - val record = ConsumerRecord<String, String>( - "test-topic", - 0, - 0, - null, - """ + val record = + ConsumerRecord<String, String>( + "test-topic", + 0, + 0, + null, + """ { "request_id": "test0123456789", "status_code": 200, "status_body": {} } - """.trimIndent() - ) + """.trimIndent(), + ) this.kafkaResponseProcessor.onMessage(record) @@ -116,7 +118,9 @@ class KafkaResponseProcessorTest { @Test fun shouldNotProcessRecordsWithoutValidStatusBody() { - this.kafkaResponseProcessor.onMessage(createKafkaRecord(requestId = "TestID1234", statusBody = null)) + this.kafkaResponseProcessor.onMessage( + createKafkaRecord(requestId = "TestID1234", statusBody = null), + ) verify(eventPublisher, never()).publishEvent(any<ResponseEvent>()) } @@ -129,21 +133,16 @@ class KafkaResponseProcessorTest { } companion object { - @JvmStatic - fun statusCodeSource(): Set<Int> { - return setOf( + fun statusCodeSource(): Set<Int> = + setOf( HttpStatus.OK, HttpStatus.CREATED, HttpStatus.BAD_REQUEST, HttpStatus.NOT_FOUND, HttpStatus.UNPROCESSABLE_ENTITY, - HttpStatus.INTERNAL_SERVER_ERROR - ) - .map { it.value() } + HttpStatus.INTERNAL_SERVER_ERROR, + ).map { it.value() } .toSet() - } - } - -}
\ No newline at end of file +} |
