summaryrefslogtreecommitdiff
path: root/src/integrationTest
diff options
context:
space:
mode:
Diffstat (limited to 'src/integrationTest')
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt73
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt4
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt70
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt77
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt136
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt51
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt273
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt265
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt88
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt73
-rw-r--r--src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt310
11 files changed, 1379 insertions, 41 deletions
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt
new file mode 100644
index 0000000..308d0cc
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/EtlProcessorArchTest.kt
@@ -0,0 +1,73 @@
+package dev.dnpm.etl.processor
+
+import com.tngtech.archunit.core.domain.JavaClasses
+import com.tngtech.archunit.core.importer.ClassFileImporter
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
+import org.junit.jupiter.api.BeforeEach
+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")
+ }
+
+ @Test
+ fun noClassesInInputPackageShouldDependOnMonitoringPackage() {
+ 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")
+
+ rule.check(noTestClasses)
+ }
+
+ @Test
+ fun noClassesInOutputPackageShouldDependOnRepositories() {
+ 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")
+
+ rule.check(noTestClasses)
+ }
+
+ @Test
+ fun repositoryClassNamesShouldEndWithRepository() {
+ 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 262aca0..c7454ed 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/config/AppConfigurationTest.kt
@@ -27,8 +27,8 @@ import dev.dnpm.etl.processor.output.RestMtbFileSender
import dev.dnpm.etl.processor.pseudonym.AnonymizingGenerator
import dev.dnpm.etl.processor.pseudonym.GpasPseudonymGenerator
import dev.dnpm.etl.processor.services.RequestProcessor
-import dev.dnpm.etl.processor.services.TokenRepository
-import dev.dnpm.etl.processor.services.TokenService
+import dev.dnpm.etl.processor.security.TokenRepository
+import dev.dnpm.etl.processor.security.TokenService
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
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 f1586d0..521ec52 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/input/MtbFileRestControllerTest.kt
@@ -22,9 +22,11 @@ package dev.dnpm.etl.processor.input
import com.fasterxml.jackson.databind.ObjectMapper
import de.ukw.ccc.bwhc.dto.*
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.security.TokenRepository
+import dev.dnpm.etl.processor.security.UserRoleRepository
import dev.dnpm.etl.processor.services.RequestProcessor
-import dev.dnpm.etl.processor.services.TokenRepository
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.ArgumentMatchers.anyString
@@ -37,6 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.context.ContextConfiguration
@@ -92,6 +95,19 @@ class MtbFileRestControllerTest {
}
@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())
+ }
+
+ @Test
fun testShouldDenyPermissionToSendMtbFile() {
mockMvc.post("/mtbfile") {
with(anonymous())
@@ -105,6 +121,19 @@ class MtbFileRestControllerTest {
}
@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())
+ }
+
+ @Test
fun testShouldGrantPermissionToDeletePatientData() {
mockMvc.delete("/mtbfile/12345678") {
with(user("onkostarserver").roles("MTBFILE"))
@@ -126,6 +155,45 @@ class MtbFileRestControllerTest {
verify(requestProcessor, never()).processDeletion(anyString())
}
+ @Nested
+ @MockBean(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())
+ }
+
+ @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())
+ }
+ }
+
companion object {
val mtbFile: MtbFile = MtbFile.builder()
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt
new file mode 100644
index 0000000..231fffe
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/monitoring/RequestRepositoryTest.kt
@@ -0,0 +1,77 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.monitoring
+
+import dev.dnpm.etl.processor.AbstractTestcontainerTest
+import dev.dnpm.etl.processor.Fingerprint
+import dev.dnpm.etl.processor.output.MtbFileSender
+import dev.dnpm.etl.processor.randomRequestId
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.test.context.TestPropertySource
+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)
+@DataJdbcTest
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Transactional
+@MockBean(MtbFileSender::class)
+@TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=buildin",
+ "app.rest.uri=http://example.com"
+ ]
+)
+class RequestRepositoryTest : AbstractTestcontainerTest() {
+
+ private lateinit var requestRepository: RequestRepository
+
+ @BeforeEach
+ fun setUp(
+ @Autowired requestRepository: RequestRepository
+ ) {
+ this.requestRepository = requestRepository
+ }
+
+ @Test
+ fun shouldSaveRequest() {
+ val request = Request(
+ randomRequestId(),
+ "TEST_12345678901",
+ "P1",
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-07-07T00:00:00Z")
+ )
+
+ requestRepository.save(request)
+ }
+
+} \ No newline at end of file
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
new file mode 100644
index 0000000..da0c55c
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/pseudonym/GpasPseudonymGeneratorTest.kt
@@ -0,0 +1,136 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.pseudonym
+
+import dev.dnpm.etl.processor.config.GPasConfigProperties
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpMethod
+import org.springframework.http.HttpStatus
+import org.springframework.retry.policy.SimpleRetryPolicy
+import org.springframework.retry.support.RetryTemplateBuilder
+import org.springframework.test.web.client.MockRestServiceServer
+import org.springframework.test.web.client.match.MockRestRequestMatchers.method
+import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
+import org.springframework.test.web.client.response.MockRestResponseCreators.withException
+import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
+import org.springframework.web.client.RestTemplate
+import java.io.IOException
+
+class GpasPseudonymGeneratorTest {
+
+ private lateinit var mockRestServiceServer: MockRestServiceServer
+ private lateinit var generator: GpasPseudonymGenerator
+ private lateinit var restTemplate: RestTemplate
+
+ @BeforeEach
+ fun setup() {
+ val retryTemplate = RetryTemplateBuilder().customPolicy(SimpleRetryPolicy(1)).build()
+ val gPasConfigProperties = GPasConfigProperties(
+ "http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate",
+ "test",
+ null,
+ null,
+ null
+ )
+
+ this.restTemplate = RestTemplate()
+ this.mockRestServiceServer = MockRestServiceServer.createServer(restTemplate)
+ this.generator = GpasPseudonymGenerator(gPasConfigProperties, retryTemplate, restTemplate)
+ }
+
+ @Test
+ fun shouldReturnExpectedPseudonym() {
+ this.mockRestServiceServer.expect {
+ method(HttpMethod.POST)
+ requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ }.andRespond {
+ withStatus(HttpStatus.OK).body(getDummyResponseBody("1234", "test", "test1234ABCDEF567890"))
+ .createResponse(it)
+ }
+
+ assertThat(this.generator.generate("ID1234")).isEqualTo("test1234ABCDEF567890")
+ }
+
+ @Test
+ fun shouldThrowExceptionIfGpasNotAvailable() {
+ this.mockRestServiceServer.expect {
+ method(HttpMethod.POST)
+ requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ }.andRespond {
+ withException(IOException("Simulated IO error")).createResponse(it)
+ }
+
+ assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
+ }
+
+ @Test
+ fun shouldThrowExceptionIfGpasDoesNotReturn2xxResponse() {
+ this.mockRestServiceServer.expect {
+ method(HttpMethod.POST)
+ requestTo("http://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ }.andRespond {
+ withStatus(HttpStatus.FOUND)
+ .header(HttpHeaders.LOCATION, "https://localhost/ttp-fhir/fhir/gpas/\$pseudonymizeAllowCreate")
+ .createResponse(it)
+ }
+
+ assertThrows<PseudonymRequestFailed> { this.generator.generate("ID1234") }
+ }
+
+ companion object {
+
+ fun getDummyResponseBody(original: String, target: String, pseudonym: String) = """{
+ "resourceType": "Parameters",
+ "parameter": [
+ {
+ "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"
+ }
+ },
+ {
+ "name": "pseudonym",
+ "valueIdentifier": {
+ "system": "https://ths-greifswald.de/gpas",
+ "value": "$pseudonym"
+ }
+ }
+ ]
+ }
+ ]
+ }""".trimIndent()
+
+ }
+} \ No newline at end of file
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 88a3a08..1aac5fd 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/services/RequestServiceIntegrationTest.kt
@@ -20,11 +20,13 @@
package dev.dnpm.etl.processor.services
import dev.dnpm.etl.processor.AbstractTestcontainerTest
+import dev.dnpm.etl.processor.Fingerprint
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 dev.dnpm.etl.processor.output.MtbFileSender
+import dev.dnpm.etl.processor.randomRequestId
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -37,7 +39,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.transaction.annotation.Transactional
import org.testcontainers.junit.jupiter.Testcontainers
import java.time.Instant
-import java.util.*
@Testcontainers
@ExtendWith(SpringExtension::class)
@@ -76,33 +77,33 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
this.requestRepository.saveAll(
listOf(
Request(
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P1",
- fingerprint = "0123456789abcdef1",
- type = RequestType.MTB_FILE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-07-07T02:00:00Z")
+ randomRequestId(),
+ "TEST_12345678901",
+ "P1",
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-07-07T02:00:00Z")
),
// Should be ignored - wrong patient ID -->
Request(
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678902",
- pid = "P2",
- fingerprint = "0123456789abcdef2",
- type = RequestType.MTB_FILE,
- status = RequestStatus.WARNING,
- processedAt = Instant.parse("2023-08-08T00:00:00Z")
+ randomRequestId(),
+ "TEST_12345678902",
+ "P2",
+ Fingerprint("0123456789abcdef2"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.parse("2023-08-08T00:00:00Z")
),
// <--
Request(
- uuid = UUID.randomUUID().toString(),
- patientId = "TEST_12345678901",
- pid = "P2",
- fingerprint = "0123456789abcdee1",
- type = RequestType.DELETE,
- status = RequestStatus.SUCCESS,
- processedAt = Instant.parse("2023-08-08T02:00:00Z")
+ randomRequestId(),
+ "TEST_12345678901",
+ "P2",
+ Fingerprint("0123456789abcdee1"),
+ RequestType.DELETE,
+ RequestStatus.SUCCESS,
+ Instant.parse("2023-08-08T02:00:00Z")
)
)
)
@@ -115,8 +116,8 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
val actual = requestService.allRequestsByPatientPseudonym("TEST_12345678901")
assertThat(actual).hasSize(2)
- assertThat(actual[0].fingerprint).isEqualTo("0123456789abcdee1")
- assertThat(actual[1].fingerprint).isEqualTo("0123456789abcdef1")
+ assertThat(actual[0].fingerprint).isEqualTo(Fingerprint("0123456789abcdee1"))
+ assertThat(actual[1].fingerprint).isEqualTo(Fingerprint("0123456789abcdef1"))
}
@Test
@@ -135,7 +136,7 @@ class RequestServiceIntegrationTest : AbstractTestcontainerTest() {
val actual = requestService.lastMtbFileRequestForPatientPseudonym("TEST_12345678901")
assertThat(actual).isNotNull
- assertThat(actual?.fingerprint).isEqualTo("0123456789abcdef1")
+ assertThat(actual?.fingerprint).isEqualTo(Fingerprint("0123456789abcdef1"))
}
} \ No newline at end of file
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 7fc0121..af4650d 100644
--- a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/ConfigControllerTest.kt
@@ -19,32 +19,51 @@
package dev.dnpm.etl.processor.web
+import com.gargoylesoftware.htmlunit.WebClient
+import com.gargoylesoftware.htmlunit.html.HtmlPage
import dev.dnpm.etl.processor.config.AppConfiguration
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
-import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
+import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult
+import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
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.services.RequestProcessor
-import dev.dnpm.etl.processor.services.TokenRepository
+import dev.dnpm.etl.processor.security.TokenService
import dev.dnpm.etl.processor.services.TransformationService
+import dev.dnpm.etl.processor.security.UserRoleService
+import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
+import org.springframework.http.MediaType.TEXT_EVENT_STREAM
+import org.springframework.security.test.context.support.WithMockUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestPropertySource
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.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.*
+import org.springframework.test.web.servlet.client.MockMvcWebTestClient
+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>
@@ -59,44 +78,48 @@ abstract class MockSink : Sinks.Many<Boolean>
)
@TestPropertySource(
properties = [
- "app.pseudonymize.generator=BUILDIN",
- "app.security.admin-user=admin",
- "app.security.admin-password={noop}very-secret",
- "app.security.enable-tokens=true"
+ "app.pseudonymize.generator=BUILDIN"
]
)
@MockBean(name = "configsUpdateProducer", classes = [MockSink::class])
@MockBean(
Generator::class,
MtbFileSender::class,
- ConnectionCheckService::class,
RequestProcessor::class,
TransformationService::class,
- TokenRepository::class,
- RestConnectionCheckService::class
+ GPasConnectionCheckService::class,
+ RestConnectionCheckService::class,
)
class ConfigControllerTest {
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 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 testShouldShowConfigPageIfLoggedIn() {
+ fun testShouldRequestConfigPageIfLoggedIn() {
mockMvc.get("/configs") {
with(user("admin").roles("ADMIN"))
accept(MediaType.TEXT_HTML)
}.andExpect {
status { isOk() }
+ view { name("configs") }
}
}
@@ -113,4 +136,228 @@ class ConfigControllerTest {
}
}
+ @Nested
+ @TestPropertySource(
+ properties = [
+ "app.security.enable-tokens=true",
+ "app.security.admin-user=admin"
+ ]
+ )
+ @MockBean(
+ 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())
+
+ assertThat(captor.firstValue).isEqualTo(42)
+ }
+
+ @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"
+ ]
+ )
+ 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()
+ }
+ }
+
+ @Nested
+ @TestPropertySource(
+ properties = [
+ "app.security.enable-tokens=false",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret"
+ ]
+ )
+ @MockBean(
+ UserRoleService::class
+ )
+ inner class WithUserRolesEnabled {
+ private lateinit var userRoleService: UserRoleService
+
+ @BeforeEach
+ fun setup(
+ @Autowired userRoleService: UserRoleService
+ ) {
+ webClient.options.isThrowExceptionOnScriptError = false
+
+ this.userRoleService = userRoleService
+ }
+
+ @Test
+ fun testShouldDeleteUserRole() {
+ mockMvc.delete("/configs/userroles/42") {
+ with(user("admin").roles("ADMIN"))
+ accept(MediaType.TEXT_HTML)
+ }.andExpect {
+ status { is2xxSuccessful() }
+ view { name("configs/userroles") }
+ }
+
+ val captor = argumentCaptor<Long>()
+ verify(userRoleService, times(1)).deleteUserRole(captor.capture())
+
+ assertThat(captor.firstValue).isEqualTo(42)
+ }
+
+ @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)
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldRenderConfigPageWithUserRoles() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/configs")
+ assertThat(
+ page.getElementById("userroles")
+ ).isNotNull
+ }
+ }
+
+ @Nested
+ inner class WithUserRolesDisabled {
+ @BeforeEach
+ fun setup() {
+ webClient.options.isThrowExceptionOnScriptError = false
+ }
+
+ @Test
+ fun testShouldRenderConfigPageWithoutUserRoles() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/configs")
+ assertThat(
+ page.getElementById("userroles")
+ ).isNull()
+ }
+ }
+
+ @Nested
+ inner class SseTest {
+ private lateinit var webClient: WebTestClient
+
+ @BeforeEach
+ fun setup(
+ applicationContext: WebApplicationContext,
+ ) {
+ this.webClient = MockMvcWebTestClient
+ .bindToApplicationContext(applicationContext).build()
+ }
+
+ @Test
+ fun testShouldRequestSSE() {
+ 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
new file mode 100644
index 0000000..a5d8771
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/HomeControllerTest.kt
@@ -0,0 +1,265 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import com.gargoylesoftware.htmlunit.WebClient
+import com.gargoylesoftware.htmlunit.html.HtmlPage
+import dev.dnpm.etl.processor.Fingerprint
+import dev.dnpm.etl.processor.NotFoundException
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.monitoring.Report
+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.randomRequestId
+import dev.dnpm.etl.processor.services.RequestService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.PageImpl
+import org.springframework.data.domain.Pageable
+import org.springframework.security.test.context.support.WithMockUser
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+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
+ ]
+)
+@TestPropertySource(
+ properties = [
+ "app.pseudonymize.generator=BUILDIN",
+ "app.security.admin-user=admin",
+ "app.security.admin-password={noop}very-secret"
+ ]
+)
+@MockBean(
+ RequestService::class
+)
+class HomeControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+ private lateinit var webClient: WebClient
+
+ 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
+ }
+
+ @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())
+ }
+
+ @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(),
+ "PSEUDO1",
+ "PATIENT1",
+ Fingerprint("ashdkasdh"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS
+ ),
+ Request(
+ 1L,
+ randomRequestId(),
+ "PSEUDO1",
+ "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,
+ "PSEUDO1",
+ "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(anyString(), any<Pageable>())).thenReturn(
+ PageImpl(
+ listOf(
+ Request(
+ 2L,
+ randomRequestId(),
+ "PSEUDO1",
+ "PATIENT1",
+ Fingerprint("ashdkasdh"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS
+ ),
+ Request(
+ 1L,
+ randomRequestId(),
+ "PSEUDO1",
+ "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 WithoutRequests {
+
+ private lateinit var requestService: RequestService
+
+ @BeforeEach
+ fun setup(
+ @Autowired requestService: RequestService
+ ) {
+ this.requestService = requestService
+
+ whenever(requestService.findAll(any<Pageable>())).thenReturn(Page.empty())
+ }
+
+ @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 testShouldThrowNotFoundExceptionForUnknownReport() {
+ val requestId = randomRequestId()
+
+ whenever(requestService.findByUuid(anyValueClass())).thenReturn(
+ Optional.empty()
+ )
+
+ assertThrows<IOException> {
+ webClient.getPage<HtmlPage>("http://localhost/report/${requestId.value}")
+ }.also {
+ assertThat(it).hasRootCauseInstanceOf(NotFoundException::class.java)
+ }
+ }
+
+ @Test
+ @WithMockUser(username = "admin", roles = ["ADMIN"])
+ fun testShouldShowEmptyPatientDetails() {
+ whenever(requestService.findRequestByPatientId(anyString(), 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)
+ }
+ }
+
+}
diff --git a/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt
new file mode 100644
index 0000000..0471543
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/LoginControllerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import com.gargoylesoftware.htmlunit.WebClient
+import com.gargoylesoftware.htmlunit.html.HtmlPage
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.security.TokenService
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+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
+
+@WebMvcTest(controllers = [LoginController::class])
+@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
+@ContextConfiguration(
+ 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"
+ ]
+)
+@MockBean(
+ TokenService::class,
+)
+class LoginControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+ private lateinit var webClient: WebClient
+
+ @BeforeEach
+ fun setup(@Autowired mockMvc: MockMvc) {
+ this.mockMvc = mockMvc
+ this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
+ }
+
+ @Test
+ fun testShouldRequestLoginPage() {
+ mockMvc.get("/login").andExpect {
+ status { isOk() }
+ view { name("login") }
+ }
+ }
+
+ @Test
+ fun testShouldShowLoginForm() {
+ val page = webClient.getPage<HtmlPage>("http://localhost/login")
+ assertThat(
+ 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
new file mode 100644
index 0000000..424a0e3
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsControllerTest.kt
@@ -0,0 +1,73 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import com.gargoylesoftware.htmlunit.WebClient
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+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
+
+@WebMvcTest(controllers = [StatisticsController::class])
+@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
+@ContextConfiguration(
+ 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"
+ ]
+)
+class StatisticsControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+ private lateinit var webClient: WebClient
+
+ @BeforeEach
+ fun setup(@Autowired mockMvc: MockMvc) {
+ this.mockMvc = mockMvc
+ this.webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build()
+ }
+
+ @Test
+ fun testShouldRequestLoginPage() {
+ mockMvc.get("/statistics").andExpect {
+ status { isOk() }
+ 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
new file mode 100644
index 0000000..e66def7
--- /dev/null
+++ b/src/integrationTest/kotlin/dev/dnpm/etl/processor/web/StatisticsRestControllerTest.kt
@@ -0,0 +1,310 @@
+/*
+ * This file is part of ETL-Processor
+ *
+ * Copyright (c) 2024 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package dev.dnpm.etl.processor.web
+
+import dev.dnpm.etl.processor.Fingerprint
+import dev.dnpm.etl.processor.config.AppConfiguration
+import dev.dnpm.etl.processor.config.AppSecurityConfiguration
+import dev.dnpm.etl.processor.monitoring.CountedState
+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.randomRequestId
+import dev.dnpm.etl.processor.services.RequestService
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.hasSize
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.whenever
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.boot.test.mock.mockito.MockBean
+import org.springframework.http.MediaType.TEXT_EVENT_STREAM
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit.jupiter.SpringExtension
+import org.springframework.test.web.reactive.server.WebTestClient
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.client.MockMvcWebTestClient
+import org.springframework.test.web.servlet.get
+import org.springframework.web.context.WebApplicationContext
+import reactor.core.publisher.Sinks
+import reactor.test.StepVerifier
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+
+
+@WebMvcTest(controllers = [StatisticsRestController::class])
+@ExtendWith(value = [MockitoExtension::class, SpringExtension::class])
+@ContextConfiguration(
+ 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"
+ ]
+)
+@MockBean(
+ RequestService::class
+)
+class StatisticsRestControllerTest {
+
+ private lateinit var mockMvc: MockMvc
+
+ private lateinit var statisticsUpdateProducer: Sinks.Many<Any>
+ private lateinit var requestService: RequestService
+
+ @BeforeEach
+ fun setup(
+ @Autowired mockMvc: MockMvc,
+ @Autowired statisticsUpdateProducer: Sinks.Many<Any>,
+ @Autowired requestService: RequestService
+ ) {
+ this.mockMvc = mockMvc
+ this.statisticsUpdateProducer = statisticsUpdateProducer
+ this.requestService = requestService
+ }
+
+ @Nested
+ inner class RequestStatesTest {
+ @Test
+ fun testShouldRequestStatesForMtbFiles() {
+ doAnswer { _ ->
+ 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))
+ }
+ }
+ }
+
+ @Test
+ fun testShouldRequestStatesForDeletes() {
+ doAnswer { _ ->
+ 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))
+ }
+ }
+ }
+ }
+
+ @Nested
+ inner class PatientRequestStatesTest {
+ @Test
+ fun testShouldRequestPatientStatesForMtbFiles() {
+ doAnswer { _ ->
+ 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))
+ }
+ }
+ }
+
+ @Test
+ fun testShouldRequestPatientStatesForDeletes() {
+ doAnswer { _ ->
+ 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))
+ }
+ }
+ }
+ }
+
+ @Nested
+ inner class LastMonthStatesTest {
+
+ @BeforeEach
+ fun setup() {
+ doAnswer { _ ->
+ listOf(
+ Request(
+ 1,
+ randomRequestId(),
+ "TEST_12345678901",
+ "P1",
+ Fingerprint("0123456789abcdef1"),
+ RequestType.MTB_FILE,
+ RequestStatus.SUCCESS,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS)
+ ),
+ Request(
+ 2,
+ randomRequestId(),
+ "TEST_12345678902",
+ "P2",
+ Fingerprint("0123456789abcdef2"),
+ RequestType.MTB_FILE,
+ RequestStatus.WARNING,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(2, ChronoUnit.DAYS)
+ ),
+ Request(
+ 3,
+ randomRequestId(),
+ "TEST_12345678901",
+ "P2",
+ Fingerprint("0123456789abcdee1"),
+ RequestType.DELETE,
+ RequestStatus.ERROR,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS)
+ ),
+ Request(
+ 4,
+ randomRequestId(),
+ "TEST_12345678902",
+ "P2",
+ Fingerprint("0123456789abcdef2"),
+ RequestType.MTB_FILE,
+ RequestStatus.DUPLICATION,
+ Instant.now().truncatedTo(ChronoUnit.DAYS).minus(1, ChronoUnit.DAYS)
+ ),
+ Request(
+ 5,
+ randomRequestId(),
+ "TEST_12345678902",
+ "P2",
+ Fingerprint("0123456789abcdef2"),
+ RequestType.DELETE,
+ RequestStatus.UNKNOWN,
+ Instant.now().truncatedTo(ChronoUnit.DAYS)
+ ),
+ )
+ }.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))
+ }
+ }
+ }
+
+ @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))
+ }
+ }
+ }
+ }
+
+ @Nested
+ inner class SseTest {
+ private lateinit var webClient: WebTestClient
+
+ @BeforeEach
+ 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)
+
+ StepVerifier.create(result.responseBody)
+ .expectComplete()
+ .verify()
+ }
+ }
+
+}