diff options
Diffstat (limited to 'src/integrationTest')
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() + } + } + +} |
