/* * 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.input import dev.dnpm.etl.processor.CustomMediaType import dev.dnpm.etl.processor.config.AppSecurityConfiguration import dev.dnpm.etl.processor.consent.ConsentEvaluation import dev.dnpm.etl.processor.consent.ConsentEvaluator import dev.dnpm.etl.processor.consent.MtbFileConsentService import dev.dnpm.etl.processor.consent.TtpConsentStatus import dev.dnpm.etl.processor.security.TokenRepository import dev.dnpm.etl.processor.security.UserRoleRepository import dev.dnpm.etl.processor.services.RequestProcessor import dev.pcvolkmer.mv64e.mtb.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest 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 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.servlet.MockMvc import org.springframework.test.web.servlet.delete import org.springframework.test.web.servlet.post import tools.jackson.databind.json.JsonMapper import java.time.Instant import java.util.* @WebMvcTest(controllers = [MtbFileRestController::class]) @ExtendWith(value = [MockitoExtension::class, SpringExtension::class]) @ContextConfiguration( classes = [ MtbFileRestController::class, MtbFileRestControllerAdvice::class, AppSecurityConfiguration::class, MtbFileConsentService::class, ] ) @MockitoBean(types = [TokenRepository::class, RequestProcessor::class, ConsentEvaluator::class]) @TestPropertySource( properties = [ "app.pseudonymize.generator=BUILDIN", "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", "app.security.enable-tokens=true", ] ) class MtbFileRestControllerTest { lateinit var mockMvc: MockMvc lateinit var requestProcessor: RequestProcessor lateinit var consentEvaluator: ConsentEvaluator @BeforeEach fun setup( @Autowired mockMvc: MockMvc, @Autowired requestProcessor: RequestProcessor, @Autowired consentEvaluator: ConsentEvaluator, ) { this.mockMvc = mockMvc this.requestProcessor = requestProcessor this.consentEvaluator = consentEvaluator doAnswer { ConsentEvaluation(TtpConsentStatus.BROAD_CONSENT_GIVEN, true) } .whenever(consentEvaluator) .check(any()) } @ParameterizedTest @ValueSource( strings = [ "/mtbfile", "/mtbfile/etl/patient-record", "/mtb", "/mtb/etl/patient-record", "/api/mtbfile", "/api/mtbfile/etl/patient-record", "/api/mtb", "/api/mtb/etl/patient-record", ] ) fun testShouldGrantPermissionToSendMtbFile(url: String) { whenever { requestProcessor.processMtbFile(any()) }.thenReturn(true) mockMvc .post(url) { with(user("onkostarserver").roles("MTBFILE")) contentType = MediaType.APPLICATION_JSON content = JsonMapper().writeValueAsString(mtbFile) } .andExpect { status { isAccepted() } } verify(requestProcessor, times(1)).processMtbFile(any()) } @ParameterizedTest @ValueSource( strings = [ "/mtbfile", "/mtbfile/etl/patient-record", "/mtb", "/mtb/etl/patient-record", "/api/mtbfile", "/api/mtbfile/etl/patient-record", "/api/mtb", "/api/mtb/etl/patient-record", ] ) fun testShouldGrantPermissionToSendMtbFileToAdminUser(url: String) { whenever { requestProcessor.processMtbFile(any()) }.thenReturn(true) mockMvc .post(url) { with(user("onkostarserver").roles("ADMIN")) contentType = MediaType.APPLICATION_JSON content = JsonMapper().writeValueAsString(mtbFile) } .andExpect { status { isAccepted() } } verify(requestProcessor, times(1)).processMtbFile(any()) } @ParameterizedTest @ValueSource( strings = [ "/mtbfile", "/mtbfile/etl/patient-record", "/mtb", "/mtb/etl/patient-record", "/api/mtbfile", "/api/mtbfile/etl/patient-record", "/api/mtb", "/api/mtb/etl/patient-record", ] ) fun testShouldGrantPermissionToSendMtbFileToUser(url: String) { whenever { requestProcessor.processMtbFile(any()) }.thenReturn(true) mockMvc .post(url) { with(user("testuser").roles("USER")) contentType = MediaType.APPLICATION_JSON content = JsonMapper().writeValueAsString(mtbFile) } .andExpect { status { isAccepted() } } verify(requestProcessor, times(1)).processMtbFile(any()) } @ParameterizedTest @ValueSource( strings = [ "/mtbfile", "/mtbfile/etl/patient-record", "/mtb", "/mtb/etl/patient-record", "/api/mtbfile", "/api/mtbfile/etl/patient-record", "/api/mtb", "/api/mtb/etl/patient-record", ] ) fun testShouldDenyPermissionToSendMtbFileForAnonymous(url: String) { mockMvc .post(url) { contentType = MediaType.APPLICATION_JSON content = JsonMapper().writeValueAsString(mtbFile) } .andExpect { status { isUnauthorized() } } verify(requestProcessor, never()).processMtbFile(any()) } @ParameterizedTest @ValueSource( strings = [ "/mtbfile/TEST_12345678", "/mtbfile/etl/patient-record/TEST_12345678", "/mtbfile/etl/patient/TEST_12345678", "/mtb/TEST_12345678", "/mtb/etl/patient-record/TEST_12345678", "/mtb/etl/patient/TEST_12345678", "/api/mtbfile/TEST_12345678", "/api/mtbfile/etl/patient-record/TEST_12345678", "/api/mtbfile/etl/patient/TEST_12345678", "/api/mtb/TEST_12345678", "/api/mtb/etl/patient-record/TEST_12345678", "/api/mtb/etl/patient/TEST_12345678", ] ) fun testShouldGrantPermissionToDeletePatientData(url: String) { mockMvc .delete(url) { with(user("onkostarserver").roles("MTBFILE")) } .andExpect { status { isAccepted() } } verify(requestProcessor, times(1)) .processDeletion(anyValueClass(), eq(TtpConsentStatus.UNKNOWN_CHECK_FILE)) } @ParameterizedTest @ValueSource( strings = [ "/mtbfile/TEST_12345678", "/mtbfile/etl/patient-record/TEST_12345678", "/mtbfile/etl/patient/TEST_12345678", "/mtb/TEST_12345678", "/mtb/etl/patient-record/TEST_12345678", "/mtb/etl/patient/TEST_12345678", "/api/mtbfile/TEST_12345678", "/api/mtbfile/etl/patient-record/TEST_12345678", "/api/mtbfile/etl/patient/TEST_12345678", "/api/mtb/TEST_12345678", "/api/mtb/etl/patient-record/TEST_12345678", "/api/mtb/etl/patient/TEST_12345678", ] ) fun testShouldDenyPermissionToDeletePatientData(url: String) { mockMvc.delete(url) { with(anonymous()) }.andExpect { status { isUnauthorized() } } verify(requestProcessor, never()).processDeletion(anyValueClass(), any()) } @ParameterizedTest @ValueSource(strings = ["[]", "null", "X", ""]) fun shouldNotAcceptNonJsonObjectPostRequestContent(requestContent: String) { mockMvc .post("/mtbfile") { content = requestContent contentType = CustomMediaType.APPLICATION_VND_DNPM_V2_MTB_JSON with(user("onkostarserver").roles("MTBFILE")) } .andExpect { status { isBadRequest() } } val result = verify(requestProcessor, times(1)).processMtbFile(any()) assertThat(result).isFalse() } @Nested @MockitoBean(types = [UserRoleRepository::class, ClientRegistrationRepository::class]) @TestPropertySource( properties = [ "app.pseudonymize.generator=BUILDIN", "app.security.admin-user=admin", "app.security.admin-password={noop}very-secret", "app.security.enable-tokens=true", "app.security.enable-oidc=true", ] ) inner class WithOidcEnabled { @ParameterizedTest @ValueSource( strings = [ "/mtbfile", "/mtbfile/etl/patient-record", "/mtb", "/mtb/etl/patient-record", "/api/mtbfile", "/api/mtbfile/etl/patient-record", "/api/mtb", "/api/mtb/etl/patient-record", ] ) fun testShouldGrantPermissionToSendMtbFileToAdminUser(url: String) { whenever { requestProcessor.processMtbFile(any()) }.thenReturn(true) mockMvc .post(url) { with(user("onkostarserver").roles("ADMIN")) contentType = MediaType.APPLICATION_JSON content = JsonMapper().writeValueAsString(mtbFile) } .andExpect { status { isAccepted() } } verify(requestProcessor, times(1)).processMtbFile(any()) } @ParameterizedTest @ValueSource( strings = [ "/mtbfile", "/mtbfile/etl/patient-record", "/mtb", "/mtb/etl/patient-record", "/api/mtbfile", "/api/mtbfile/etl/patient-record", "/api/mtb", "/api/mtb/etl/patient-record", ] ) fun testShouldGrantPermissionToSendMtbFileToUser(url: String) { whenever { requestProcessor.processMtbFile(any()) }.thenReturn(true) mockMvc .post(url) { with(user("onkostarserver").roles("USER")) contentType = MediaType.APPLICATION_JSON content = JsonMapper().writeValueAsString(mtbFile) } .andExpect { status { isAccepted() } } verify(requestProcessor, times(1)).processMtbFile(any()) } } companion object { val mtbFile = Mtb.builder() .patient(Patient.builder().id("PID").build()) .episodesOfCare( listOf( MtbEpisodeOfCare.builder() .id("1") .patient(Reference.builder().id("PID").build()) .period( PeriodDate.builder() .start(Date.from(Instant.parse("2023-08-08T02:00:00.00Z"))) .build() ) .build() ) ) .build() } }