/* * This file is part of ETL-Processor * * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken * Copyright (c) 2024-2026 Paul-Christian Volkmer, 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 . */ package dev.dnpm.etl.processor.web import dev.dnpm.etl.processor.config.AppConfiguration import dev.dnpm.etl.processor.config.AppSecurityConfiguration import dev.dnpm.etl.processor.monitoring.ConnectionCheckResult import dev.dnpm.etl.processor.monitoring.GIcsConnectionCheckService 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.security.TokenService import dev.dnpm.etl.processor.security.UserRoleService import dev.dnpm.etl.processor.services.RequestProcessor import dev.dnpm.etl.processor.services.TransformationService import org.assertj.core.api.Assertions.assertThat import org.htmlunit.WebClient import org.htmlunit.html.HtmlPage 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.webmvc.test.autoconfigure.WebMvcTest 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.bean.override.mockito.MockitoBean import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.returnResult 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 @WebMvcTest(controllers = [ConfigController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( classes = [ConfigController::class, AppConfiguration::class, AppSecurityConfiguration::class] ) @TestPropertySource(properties = [ "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", "app.pseudonymize.generator=BUILDIN" ]) @MockitoBean(name = "configsUpdateProducer", types = [MockSink::class]) @MockitoBean( types = [ Generator::class, MtbFileSender::class, RequestProcessor::class, TransformationService::class, GPasConnectionCheckService::class, RestConnectionCheckService::class, GIcsConnectionCheckService::class, ] ) class ConfigControllerTest { private lateinit var mockMvc: MockMvc private lateinit var webClient: WebClient private lateinit var requestProcessor: RequestProcessor private lateinit var connectionCheckUpdateProducer: Sinks.Many @BeforeEach fun setup( @Autowired mockMvc: MockMvc, @Autowired requestProcessor: RequestProcessor, @Autowired connectionCheckUpdateProducer: Sinks.Many, @Autowired webApplicationContext: WebApplicationContext, ) { this.mockMvc = mockMvc this.webClient = MockMvcWebClientBuilder.webAppContextSetup(webApplicationContext).build() this.requestProcessor = requestProcessor this.connectionCheckUpdateProducer = connectionCheckUpdateProducer this.webClient.options.isJavaScriptEnabled = false this.webClient.options.isCssEnabled = false this.webClient.options.isThrowExceptionOnScriptError = false } @Test fun testShouldRequestConfigPageIfLoggedIn() { mockMvc .get("/configs") { with(user("admin").roles("ADMIN")) accept(MediaType.TEXT_HTML) } .andExpect { status { isOk() } view { name("configs") } } } @Test fun testShouldRedirectToLoginPageIfNotLoggedIn() { mockMvc .get("/configs") { with(anonymous()) accept(MediaType.TEXT_HTML) } .andExpect { status { isFound() } header { stringValues(HttpHeaders.LOCATION, "/login") } } } @Nested @TestPropertySource( properties = ["app.security.enable-tokens=true", "app.security.admin-user=admin"] ) @MockitoBean(types = [TokenService::class]) inner class WithTokensEnabled { private lateinit var tokenService: TokenService @BeforeEach fun setup(@Autowired tokenService: TokenService) { webClient.options.isThrowExceptionOnScriptError = false this.tokenService = tokenService } @Test fun testShouldSaveNewToken() { mockMvc .post("/configs/tokens") { with(user("admin").roles("ADMIN")) accept(MediaType.TEXT_HTML) contentType = MediaType.APPLICATION_FORM_URLENCODED content = "name=Testtoken" } .andExpect { status { is2xxSuccessful() } view { name("configs/tokens") } } val captor = argumentCaptor() 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() 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() verify(tokenService, times(1)).deleteToken(captor.capture()) assertThat(captor.firstValue).isEqualTo(42) } @Test @WithMockUser(username = "admin", roles = ["ADMIN"]) fun testShouldRenderConfigPageWithTokens() { val page = webClient.getPage("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("http://localhost/configs") assertThat(page.getElementById("tokens")).isNull() } } @Nested @TestPropertySource( properties = [ "app.security.enable-tokens=false", "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", ] ) @MockitoBean(types = [UserRoleService::class]) inner class WithUserRolesEnabled { private lateinit var userRoleService: UserRoleService @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() 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() val roleCaptor = argumentCaptor() 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("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("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 testShouldRequestGPasSSE() { val expectedEvent = ConnectionCheckResult.GPasConnectionCheckResult(true, Instant.now(), Instant.now()) connectionCheckUpdateProducer.tryEmitNext(expectedEvent) connectionCheckUpdateProducer.emitComplete { _, _ -> true } val result = webClient .get() .uri("http://localhost/configs/events") .accept(TEXT_EVENT_STREAM) .exchange() .expectStatus() .isOk() .expectHeader() .contentType(TEXT_EVENT_STREAM) .returnResult() StepVerifier.create(result.responseBody).expectNext(expectedEvent).expectComplete().verify() } } }