From 3ab05805f21d329490d226553e5de3374e439681 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Wed, 8 Apr 2026 11:20:41 +0200 Subject: [PATCH 1/9] Add unit tests for CaseService authorization logic --- .../application/service/CaseServiceTest.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/application/service/CaseServiceTest.java diff --git a/src/test/java/org/example/projektarendehantering/application/service/CaseServiceTest.java b/src/test/java/org/example/projektarendehantering/application/service/CaseServiceTest.java new file mode 100644 index 0000000..50ee8c4 --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/application/service/CaseServiceTest.java @@ -0,0 +1,144 @@ +package org.example.projektarendehantering.application.service; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.*; +import org.example.projektarendehantering.presentation.dto.CaseDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CaseServiceTest { + + @Mock + private CaseRepository caseRepository; + @Mock + private CaseMapper caseMapper; + @Mock + private PatientRepository patientRepository; + @Mock + private CaseNoteRepository caseNoteRepository; + @Mock + private EmployeeRepository employeeRepository; + + @InjectMocks + private CaseService caseService; + + private Actor doctorActor; + private Actor nurseActor; + private Actor managerActor; + private Actor patientActor; + private UUID caseId; + private CaseEntity caseEntity; + + @BeforeEach + void setUp() { + UUID doctorId = UUID.randomUUID(); + UUID nurseId = UUID.randomUUID(); + UUID managerId = UUID.randomUUID(); + UUID patientId = UUID.randomUUID(); + + doctorActor = new Actor(doctorId, Role.DOCTOR); + nurseActor = new Actor(nurseId, Role.NURSE); + managerActor = new Actor(managerId, Role.MANAGER); + patientActor = new Actor(patientId, Role.PATIENT); + + caseId = UUID.randomUUID(); + caseEntity = new CaseEntity(); + caseEntity.setId(caseId); + caseEntity.setOwnerId(doctorId); + caseEntity.setHandlerId(nurseId); + + PatientEntity patient = new PatientEntity(); + patient.setId(patientId); + caseEntity.setPatient(patient); + } + + @Test + void getCase_shouldAllowOwnerToRead() { + when(caseRepository.findById(caseId)).thenReturn(Optional.of(caseEntity)); + when(caseMapper.toDTO(caseEntity)).thenReturn(new CaseDTO()); + + CaseDTO result = caseService.getCase(doctorActor, caseId).orElseThrow(); + + assertThat(result).isNotNull(); + verify(caseRepository).findById(caseId); + } + + @Test + void getCase_shouldAllowHandlerToRead() { + when(caseRepository.findById(caseId)).thenReturn(Optional.of(caseEntity)); + when(caseMapper.toDTO(caseEntity)).thenReturn(new CaseDTO()); + + CaseDTO result = caseService.getCase(nurseActor, caseId).orElseThrow(); + + assertThat(result).isNotNull(); + } + + @Test + void getCase_shouldAllowManagerToRead() { + when(caseRepository.findById(caseId)).thenReturn(Optional.of(caseEntity)); + when(caseMapper.toDTO(caseEntity)).thenReturn(new CaseDTO()); + + CaseDTO result = caseService.getCase(managerActor, caseId).orElseThrow(); + + assertThat(result).isNotNull(); + } + + @Test + void getCase_shouldAllowPatientToReadOwnCase() { + when(caseRepository.findById(caseId)).thenReturn(Optional.of(caseEntity)); + when(caseMapper.toDTO(caseEntity)).thenReturn(new CaseDTO()); + + CaseDTO result = caseService.getCase(patientActor, caseId).orElseThrow(); + + assertThat(result).isNotNull(); + } + + @Test + void getCase_shouldDenyUnauthorizedAccess() { + Actor unauthorizedActor = new Actor(UUID.randomUUID(), Role.DOCTOR); + when(caseRepository.findById(caseId)).thenReturn(Optional.of(caseEntity)); + + assertThatThrownBy(() -> caseService.getCase(unauthorizedActor, caseId)) + .isInstanceOf(NotAuthorizedException.class) + .hasMessageContaining("Not allowed to read this case"); + } + + @Test + void createCase_shouldAllowDoctor() { + CaseDTO dto = new CaseDTO(); + dto.setPatientId(patientActor.userId()); + PatientEntity patient = new PatientEntity(); + patient.setId(patientActor.userId()); + + when(caseMapper.toEntity(dto)).thenReturn(new CaseEntity()); + when(patientRepository.findById(patientActor.userId())).thenReturn(Optional.of(patient)); + when(caseRepository.save(any(CaseEntity.class))).thenAnswer(i -> i.getArgument(0)); + when(caseMapper.toDTO(any(CaseEntity.class))).thenReturn(new CaseDTO()); + + caseService.createCase(doctorActor, dto); + + verify(caseRepository).save(any(CaseEntity.class)); + } + + @Test + void createCase_shouldDenyNurse() { + CaseDTO dto = new CaseDTO(); + assertThatThrownBy(() -> caseService.createCase(nurseActor, dto)) + .isInstanceOf(NotAuthorizedException.class); + } +} From 8f90419eedef18e2c5d228dc176a927a52add0e3 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Wed, 8 Apr 2026 11:33:12 +0200 Subject: [PATCH 2/9] Add unit tests for SecurityActorAdapter --- .../security/SecurityActorAdapterTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java diff --git a/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java new file mode 100644 index 0000000..429c2aa --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java @@ -0,0 +1,94 @@ +package org.example.projektarendehantering.infrastructure.security; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; +import org.example.projektarendehantering.common.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SecurityActorAdapterTest { + + @InjectMocks + private SecurityActorAdapter securityActorAdapter; + + @Mock + private SecurityContext securityContext; + + @BeforeEach + void setUp() { + SecurityContextHolder.setContext(securityContext); + } + + @Test + void currentUser_shouldReturnManagerActor() { + Authentication auth = mock(Authentication.class); + when(auth.isAuthenticated()).thenReturn(true); + when(auth.getName()).thenReturn("managerUser"); + Collection authorities = List.of(new SimpleGrantedAuthority("ROLE_MANAGER")); + doReturn(authorities).when(auth).getAuthorities(); + when(securityContext.getAuthentication()).thenReturn(auth); + + Actor actor = securityActorAdapter.currentUser(); + + assertThat(actor.role()).isEqualTo(Role.MANAGER); + UUID expectedId = UUID.nameUUIDFromBytes("managerUser".getBytes(StandardCharsets.UTF_8)); + assertThat(actor.userId()).isEqualTo(expectedId); + } + + @Test + void currentUser_shouldReturnPatientActor_whenNoRoles() { + Authentication auth = mock(Authentication.class); + when(auth.isAuthenticated()).thenReturn(true); + when(auth.getName()).thenReturn("patientUser"); + doReturn(List.of()).when(auth).getAuthorities(); + when(securityContext.getAuthentication()).thenReturn(auth); + + Actor actor = securityActorAdapter.currentUser(); + + assertThat(actor.role()).isEqualTo(Role.PATIENT); + } + + @Test + void currentUser_shouldThrowException_whenNotAuthenticated() { + Authentication auth = mock(Authentication.class); + when(auth.isAuthenticated()).thenReturn(false); + when(securityContext.getAuthentication()).thenReturn(auth); + + assertThatThrownBy(() -> securityActorAdapter.currentUser()) + .isInstanceOf(NotAuthorizedException.class); + } + + @Test + void currentUser_shouldThrowException_whenAuthIsNull() { + when(securityContext.getAuthentication()).thenReturn(null); + + assertThatThrownBy(() -> securityActorAdapter.currentUser()) + .isInstanceOf(NotAuthorizedException.class); + } + + // Helper because getAuthorities() is wildcard + @SuppressWarnings("unchecked") + private org.mockito.stubbing.Stubber doReturn(Object value) { + return org.mockito.Mockito.doReturn(value); + } +} From 6e72346f975b3545fabc6f0ca6b0954784574e26 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Wed, 8 Apr 2026 12:46:41 +0200 Subject: [PATCH 3/9] Add unit tests for EmployeeService --- .../service/EmployeeServiceTest.java | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/application/service/EmployeeServiceTest.java diff --git a/src/test/java/org/example/projektarendehantering/application/service/EmployeeServiceTest.java b/src/test/java/org/example/projektarendehantering/application/service/EmployeeServiceTest.java new file mode 100644 index 0000000..6e6b5af --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/application/service/EmployeeServiceTest.java @@ -0,0 +1,92 @@ +package org.example.projektarendehantering.application.service; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeEntity; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; +import org.example.projektarendehantering.presentation.dto.EmployeeCreateDTO; +import org.example.projektarendehantering.presentation.dto.EmployeeDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class EmployeeServiceTest { + + @Mock + private EmployeeRepository employeeRepository; + @Mock + private EmployeeMapper employeeMapper; + + @InjectMocks + private EmployeeService employeeService; + + private Actor managerActor; + private Actor doctorActor; + + @BeforeEach + void setUp() { + managerActor = new Actor(UUID.randomUUID(), Role.MANAGER); + doctorActor = new Actor(UUID.randomUUID(), Role.DOCTOR); + } + + @Test + void getAllEmployees_shouldAllowManager() { + when(employeeRepository.findAll()).thenReturn(List.of(new EmployeeEntity())); + when(employeeMapper.toDTO(any())).thenReturn(new EmployeeDTO(UUID.randomUUID(), "Name", Role.DOCTOR, null)); + + List result = employeeService.getAllEmployees(managerActor); + + assertThat(result).isNotEmpty(); + verify(employeeRepository).findAll(); + } + + @Test + void getAllEmployees_shouldDenyDoctor() { + assertThatThrownBy(() -> employeeService.getAllEmployees(doctorActor)) + .isInstanceOf(NotAuthorizedException.class) + .hasMessageContaining("Not allowed to access employees"); + } + + @Test + void createEmployee_shouldAllowManager() { + EmployeeCreateDTO dto = new EmployeeCreateDTO(); + EmployeeEntity entity = new EmployeeEntity(); + EmployeeDTO resultDTO = new EmployeeDTO(UUID.randomUUID(), "Name", Role.DOCTOR, null); + + when(employeeMapper.toEntity(dto)).thenReturn(entity); + when(employeeRepository.save(any())).thenReturn(entity); + when(employeeMapper.toDTO(entity)).thenReturn(resultDTO); + + EmployeeDTO result = employeeService.createEmployee(managerActor, dto); + + assertThat(result).isNotNull(); + verify(employeeRepository).save(any()); + } + + @Test + void createEmployee_shouldDenyDoctor() { + EmployeeCreateDTO dto = new EmployeeCreateDTO(); + assertThatThrownBy(() -> employeeService.createEmployee(doctorActor, dto)) + .isInstanceOf(NotAuthorizedException.class); + } + + @Test + void requireCanManageEmployees_shouldThrow_whenActorIsNull() { + assertThatThrownBy(() -> employeeService.getAllEmployees(null)) + .isInstanceOf(NotAuthorizedException.class) + .hasMessageContaining("Missing actor"); + } +} From c15582e1610a1093d165f45e86eeed3c9681fd63 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Wed, 8 Apr 2026 13:30:21 +0200 Subject: [PATCH 4/9] Add integration tests for CaseController --- .../presentation/rest/CaseControllerTest.java | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/presentation/rest/CaseControllerTest.java diff --git a/src/test/java/org/example/projektarendehantering/presentation/rest/CaseControllerTest.java b/src/test/java/org/example/projektarendehantering/presentation/rest/CaseControllerTest.java new file mode 100644 index 0000000..3a94aff --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/presentation/rest/CaseControllerTest.java @@ -0,0 +1,131 @@ +package org.example.projektarendehantering.presentation.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.example.projektarendehantering.application.service.CaseService; +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; +import org.example.projektarendehantering.presentation.dto.CaseDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +@SpringBootTest +class CaseControllerTest { + + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext context; + + @MockitoBean + private CaseService caseService; + + @MockitoBean + private SecurityActorAdapter securityActorAdapter; + + private ObjectMapper objectMapper = new ObjectMapper(); + + private Actor doctorActor; + private UUID caseId; + + @BeforeEach + void setUp() { + objectMapper.findAndRegisterModules(); // Ensure Java 8 time etc are supported if needed + mockMvc = webAppContextSetup(context) + .apply(springSecurity()) + .build(); + caseId = UUID.randomUUID(); + doctorActor = new Actor(UUID.randomUUID(), Role.DOCTOR); + when(securityActorAdapter.currentUser()).thenReturn(doctorActor); + } + + @Test + @WithMockUser(roles = "DOCTOR") + void getAllCases_shouldReturnList() throws Exception { + CaseDTO caseDTO = new CaseDTO(); + caseDTO.setId(caseId); + caseDTO.setDescription("Test Case"); + + when(caseService.getAllCases(doctorActor)).thenReturn(List.of(caseDTO)); + + mockMvc.perform(get("/api/cases")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(caseId.toString())) + .andExpect(jsonPath("$[0].description").value("Test Case")); + } + + @Test + @WithMockUser(roles = "DOCTOR") + void getCase_shouldReturnCase_whenExists() throws Exception { + CaseDTO caseDTO = new CaseDTO(); + caseDTO.setId(caseId); + + when(caseService.getCase(eq(doctorActor), eq(caseId))).thenReturn(Optional.of(caseDTO)); + + mockMvc.perform(get("/api/cases/{id}", caseId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(caseId.toString())); + } + + @Test + @WithMockUser(roles = "DOCTOR") + void getCase_shouldReturnNotFound_whenDoesNotExist() throws Exception { + when(caseService.getCase(eq(doctorActor), eq(caseId))).thenReturn(Optional.empty()); + + mockMvc.perform(get("/api/cases/{id}", caseId)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "DOCTOR") + void createCase_shouldReturnCreatedCase() throws Exception { + CaseDTO inputDTO = new CaseDTO(); + inputDTO.setDescription("New Case"); + inputDTO.setPatientId(UUID.randomUUID()); + + CaseDTO outputDTO = new CaseDTO(); + outputDTO.setId(UUID.randomUUID()); + outputDTO.setDescription("New Case"); + + when(caseService.createCase(eq(doctorActor), any(CaseDTO.class))).thenReturn(outputDTO); + + mockMvc.perform(post("/api/cases") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(inputDTO))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.description").value("New Case")); + } + + @Test + void getAllCases_shouldReturnUnauthorized_whenNotLoggedIn() throws Exception { + // Without @WithMockUser + mockMvc.perform(get("/api/cases")) + .andExpect(status().is3xxRedirection()); // Redirect to login in OAuth2 setup + } +} From c3a422b18f5e21667c66a0fb554379ddaf77ba08 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Wed, 8 Apr 2026 13:44:57 +0200 Subject: [PATCH 5/9] Add integration tests for EmployeeController --- .../rest/EmployeeControllerTest.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/presentation/rest/EmployeeControllerTest.java diff --git a/src/test/java/org/example/projektarendehantering/presentation/rest/EmployeeControllerTest.java b/src/test/java/org/example/projektarendehantering/presentation/rest/EmployeeControllerTest.java new file mode 100644 index 0000000..d36bc29 --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/presentation/rest/EmployeeControllerTest.java @@ -0,0 +1,105 @@ +package org.example.projektarendehantering.presentation.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.example.projektarendehantering.application.service.EmployeeService; +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; +import org.example.projektarendehantering.presentation.dto.EmployeeCreateDTO; +import org.example.projektarendehantering.presentation.dto.EmployeeDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +@SpringBootTest +class EmployeeControllerTest { + + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext context; + + @MockitoBean + private EmployeeService employeeService; + + @MockitoBean + private SecurityActorAdapter securityActorAdapter; + + private ObjectMapper objectMapper = new ObjectMapper(); + + private Actor managerActor; + + @BeforeEach + void setUp() { + mockMvc = webAppContextSetup(context) + .apply(springSecurity()) + .build(); + managerActor = new Actor(UUID.randomUUID(), Role.MANAGER); + when(securityActorAdapter.currentUser()).thenReturn(managerActor); + } + + @Test + @WithMockUser(roles = "MANAGER") + void getAllEmployees_shouldReturnList() throws Exception { + EmployeeDTO dto = new EmployeeDTO(UUID.randomUUID(), "Manager Name", Role.MANAGER, null); + + when(employeeService.getAllEmployees(managerActor)).thenReturn(List.of(dto)); + + mockMvc.perform(get("/api/employees")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].displayName").value("Manager Name")); + } + + @Test + @WithMockUser(roles = "MANAGER") + void createEmployee_shouldReturnCreatedEmployee() throws Exception { + EmployeeCreateDTO input = new EmployeeCreateDTO(); + input.setDisplayName("New Employee"); + input.setRole(Role.DOCTOR); + + EmployeeDTO output = new EmployeeDTO(UUID.randomUUID(), "New Employee", Role.DOCTOR, null); + + when(employeeService.createEmployee(eq(managerActor), any(EmployeeCreateDTO.class))).thenReturn(output); + + mockMvc.perform(post("/api/employees") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(input))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.displayName").value("New Employee")); + } + + @Test + @WithMockUser(roles = "MANAGER") + void getEmployee_shouldReturnEmployee_whenExists() throws Exception { + UUID empId = UUID.randomUUID(); + EmployeeDTO dto = new EmployeeDTO(empId, "Some Name", Role.NURSE, null); + + when(employeeService.getEmployee(eq(managerActor), eq(empId))).thenReturn(Optional.of(dto)); + + mockMvc.perform(get("/api/employees/{id}", empId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(empId.toString())); + } +} From 22a977c46917404f9ff257e5dbddd187f4e45483 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Wed, 8 Apr 2026 14:41:55 +0200 Subject: [PATCH 6/9] Add integration tests for Auditing system --- .../AuditIntegrationTest.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java diff --git a/src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java b/src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java new file mode 100644 index 0000000..8e08cb9 --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java @@ -0,0 +1,113 @@ +package org.example.projektarendehantering; + +import org.example.projektarendehantering.application.service.AuditService; +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.AuditEventEntity; +import org.example.projektarendehantering.infrastructure.persistence.AuditEventRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +@SpringBootTest +@Transactional +class AuditIntegrationTest { + + @Autowired + private WebApplicationContext context; + + @Autowired + private AuditEventRepository auditEventRepository; + + @Autowired + private AuditService auditService; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @Test + @WithMockUser(username = "testuser", roles = {"MANAGER"}) + void anyRequest_shouldBeAudited() throws Exception { + long countBefore = auditEventRepository.count(); + + mockMvc.perform(get("/api/cases")) + .andExpect(status().isOk()); + + List events = auditEventRepository.findAll(); + assertThat(events.size()).isGreaterThan((int) countBefore); + + AuditEventEntity latest = events.get(events.size() - 1); + assertThat(latest.getRequestPath()).isEqualTo("/api/cases"); + assertThat(latest.getHttpMethod()).isEqualTo("GET"); + } + + @Test + void record_shouldSanitizeSensitiveQueryParameters() { + AuditEventEntity event = new AuditEventEntity(); + event.setRequestPath("/api/login"); + event.setHttpMethod("POST"); + event.setQueryString("username=oscar&password=secretPassword123&token=abc-123"); + + auditService.record(event); + + List events = auditEventRepository.findAll(); + AuditEventEntity saved = events.get(events.size() - 1); + + assertThat(saved.getQueryString()) + .contains("username=oscar") + .contains("password=[REDACTED]") + .contains("token=[REDACTED]"); + } + + @Test + void record_shouldSanitizeSensitiveJsonPayload() { + AuditEventEntity event = new AuditEventEntity(); + event.setRequestPath("/api/users"); + event.setHttpMethod("POST"); + event.setQueryString("{\"name\": \"Oscar\", \"secret\": \"top-secret\", \"ssn\": \"12345\"}"); + + auditService.record(event); + + List events = auditEventRepository.findAll(); + AuditEventEntity saved = events.get(events.size() - 1); + + assertThat(saved.getQueryString()) + .contains("\"name\":\"Oscar\"") + .contains("\"secret\":\"[REDACTED]\"") + .contains("\"ssn\":\"[REDACTED]\""); + } + + @Test + @WithMockUser(username = "doctor", roles = {"DOCTOR"}) + void auditInterceptor_shouldCaptureCaseId_fromUri() throws Exception { + UUID caseId = UUID.randomUUID(); + + mockMvc.perform(get("/api/cases/{id}", caseId)) + .andExpect(status().isNotFound()); // Case doesn't exist, but that's fine for auditing + + List events = auditEventRepository.findAll(); + AuditEventEntity latest = events.get(events.size() - 1); + + assertThat(latest.getCaseId()).isEqualTo(caseId); + } +} From 0a69e2bc13600f4277d9bed1f708b508b8625d8f Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Wed, 8 Apr 2026 20:33:21 +0200 Subject: [PATCH 7/9] Add integration tests for AuditController --- .../rest/AuditControllerTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java diff --git a/src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java b/src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java new file mode 100644 index 0000000..d13b807 --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java @@ -0,0 +1,94 @@ +package org.example.projektarendehantering.presentation.rest; + +import org.example.projektarendehantering.application.service.AuditService; +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; +import org.example.projektarendehantering.presentation.dto.AuditEventDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +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.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; + +import java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; + +@SpringBootTest +class AuditControllerTest { + + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext context; + + @MockitoBean + private AuditService auditService; + + @MockitoBean + private SecurityActorAdapter securityActorAdapter; + + private Actor managerActor; + + @BeforeEach + void setUp() { + mockMvc = webAppContextSetup(context) + .apply(springSecurity()) + .build(); + managerActor = new Actor(UUID.randomUUID(), Role.MANAGER); + when(securityActorAdapter.currentUser()).thenReturn(managerActor); + } + + @Test + @WithMockUser(roles = "MANAGER") + void list_shouldReturnAuditEvents() throws Exception { + AuditEventDTO dto = new AuditEventDTO(); + dto.setRequestPath("/api/cases"); + dto.setHttpMethod("GET"); + + Page page = new PageImpl<>(List.of(dto)); + + when(auditService.listEvents(eq(managerActor), any(), any(), any(), any(Pageable.class))) + .thenReturn(page); + + mockMvc.perform(get("/api/audit")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].requestPath").value("/api/cases")); + } + + @Test + @WithMockUser(roles = "MANAGER") + void list_withFilters_shouldPassParameters() throws Exception { + UUID caseId = UUID.randomUUID(); + + when(auditService.listEvents(eq(managerActor), any(), any(), eq(caseId), any(Pageable.class))) + .thenReturn(Page.empty()); + + mockMvc.perform(get("/api/audit") + .param("caseId", caseId.toString()) + .param("page", "1") + .param("size", "10")) + .andExpect(status().isOk()); + } + + @Test + void list_shouldBeUnauthorized_whenNotLoggedIn() throws Exception { + mockMvc.perform(get("/api/audit")) + .andExpect(status().is3xxRedirection()); + } +} From 56578cf1b9f5c1a4ee707c8c8e01d67f19814959 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Thu, 9 Apr 2026 09:42:38 +0200 Subject: [PATCH 8/9] Fix flaky audit tests with deterministic selection and add SecurityActorAdapterTest --- .../AuditIntegrationTest.java | 24 ++- .../application/service/AuditServiceTest.java | 110 ++++++++++++ .../security/SecurityActorAdapterTest.java | 167 ++++++++++++++---- 3 files changed, 262 insertions(+), 39 deletions(-) create mode 100644 src/test/java/org/example/projektarendehantering/application/service/AuditServiceTest.java diff --git a/src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java b/src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java index 8e08cb9..26c5188 100644 --- a/src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java +++ b/src/test/java/org/example/projektarendehantering/AuditIntegrationTest.java @@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; +import java.util.Comparator; import java.util.List; import java.util.UUID; @@ -56,7 +57,10 @@ void anyRequest_shouldBeAudited() throws Exception { List events = auditEventRepository.findAll(); assertThat(events.size()).isGreaterThan((int) countBefore); - AuditEventEntity latest = events.get(events.size() - 1); + AuditEventEntity latest = events.stream() + .max(Comparator.comparing(AuditEventEntity::getOccurredAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(AuditEventEntity::getId, Comparator.nullsLast(Comparator.naturalOrder()))) + .orElseThrow(); assertThat(latest.getRequestPath()).isEqualTo("/api/cases"); assertThat(latest.getHttpMethod()).isEqualTo("GET"); } @@ -70,8 +74,10 @@ void record_shouldSanitizeSensitiveQueryParameters() { auditService.record(event); - List events = auditEventRepository.findAll(); - AuditEventEntity saved = events.get(events.size() - 1); + AuditEventEntity saved = auditEventRepository.findAll().stream() + .max(Comparator.comparing(AuditEventEntity::getOccurredAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(AuditEventEntity::getId, Comparator.nullsLast(Comparator.naturalOrder()))) + .orElseThrow(); assertThat(saved.getQueryString()) .contains("username=oscar") @@ -88,8 +94,10 @@ void record_shouldSanitizeSensitiveJsonPayload() { auditService.record(event); - List events = auditEventRepository.findAll(); - AuditEventEntity saved = events.get(events.size() - 1); + AuditEventEntity saved = auditEventRepository.findAll().stream() + .max(Comparator.comparing(AuditEventEntity::getOccurredAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(AuditEventEntity::getId, Comparator.nullsLast(Comparator.naturalOrder()))) + .orElseThrow(); assertThat(saved.getQueryString()) .contains("\"name\":\"Oscar\"") @@ -105,8 +113,10 @@ void auditInterceptor_shouldCaptureCaseId_fromUri() throws Exception { mockMvc.perform(get("/api/cases/{id}", caseId)) .andExpect(status().isNotFound()); // Case doesn't exist, but that's fine for auditing - List events = auditEventRepository.findAll(); - AuditEventEntity latest = events.get(events.size() - 1); + AuditEventEntity latest = auditEventRepository.findAll().stream() + .max(Comparator.comparing(AuditEventEntity::getOccurredAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(AuditEventEntity::getId, Comparator.nullsLast(Comparator.naturalOrder()))) + .orElseThrow(); assertThat(latest.getCaseId()).isEqualTo(caseId); } diff --git a/src/test/java/org/example/projektarendehantering/application/service/AuditServiceTest.java b/src/test/java/org/example/projektarendehantering/application/service/AuditServiceTest.java new file mode 100644 index 0000000..5c7c4c6 --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/application/service/AuditServiceTest.java @@ -0,0 +1,110 @@ +package org.example.projektarendehantering.application.service; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.AuditEventEntity; +import org.example.projektarendehantering.infrastructure.persistence.AuditEventRepository; +import org.example.projektarendehantering.infrastructure.persistence.CaseEntity; +import org.example.projektarendehantering.infrastructure.persistence.CaseRepository; +import org.example.projektarendehantering.presentation.dto.AuditEventDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AuditServiceTest { + + @Mock + private AuditEventRepository auditEventRepository; + @Mock + private AuditEventMapper auditEventMapper; + @Mock + private CaseRepository caseRepository; + + @InjectMocks + private AuditService auditService; + + private Actor managerActor; + private Actor doctorActor; + private UUID caseId; + + @BeforeEach + void setUp() { + managerActor = new Actor(UUID.randomUUID(), Role.MANAGER); + doctorActor = new Actor(UUID.randomUUID(), Role.DOCTOR); + caseId = UUID.randomUUID(); + } + + @Test + void listEvents_shouldAllowManagerToSeeAll() { + Page page = new PageImpl<>(List.of(new AuditEventEntity())); + when(auditEventRepository.findAllByOccurredAtBetweenOrderByOccurredAtDesc(any(), any(), any())) + .thenReturn(page); + when(auditEventMapper.toDTO(any())).thenReturn(new AuditEventDTO()); + + Page result = auditService.listEvents(managerActor, null, null, null, Pageable.unpaged()); + + assertThat(result.getContent()).hasSize(1); + } + + @Test + void listEvents_shouldAllowDoctorToSeeOnlyTheirCases() { + CaseEntity caseEntity = new CaseEntity(); + caseEntity.setId(caseId); + + when(caseRepository.findAllByOwnerId(doctorActor.userId())).thenReturn(List.of(caseEntity)); + when(auditEventRepository.findAllByCaseIdInAndOccurredAtBetweenOrderByOccurredAtDesc(eq(Set.of(caseId)), any(), any(), any())) + .thenReturn(new PageImpl<>(List.of(new AuditEventEntity()))); + when(auditEventMapper.toDTO(any())).thenReturn(new AuditEventDTO()); + + Page result = auditService.listEvents(doctorActor, null, null, null, Pageable.unpaged()); + + assertThat(result.getContent()).hasSize(1); + } + + @Test + void listEvents_shouldDenyDoctorAccessToUnownedCase() { + when(caseRepository.findAllByOwnerId(doctorActor.userId())).thenReturn(Collections.emptyList()); + + assertThatThrownBy(() -> auditService.listEvents(doctorActor, null, null, caseId, Pageable.unpaged())) + .isInstanceOf(NotAuthorizedException.class) + .hasMessageContaining("Not allowed to view audit events for this case"); + } + + @Test + void listEvents_shouldDenyPatient() { + Actor patientActor = new Actor(UUID.randomUUID(), Role.PATIENT); + + assertThatThrownBy(() -> auditService.listEvents(patientActor, null, null, null, Pageable.unpaged())) + .isInstanceOf(NotAuthorizedException.class); + } + + @Test + void listEvents_shouldThrowOnInvalidRange() { + Instant from = Instant.now(); + Instant to = from.minusSeconds(10); + + assertThatThrownBy(() -> auditService.listEvents(managerActor, from, to, null, Pageable.unpaged())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid time range"); + } +} diff --git a/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java index 429c2aa..03f9fe0 100644 --- a/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java +++ b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java @@ -3,6 +3,9 @@ import org.example.projektarendehantering.common.Actor; import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeEntity; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,80 +13,180 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; import java.nio.charset.StandardCharsets; -import java.util.Collection; +import java.time.Instant; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class SecurityActorAdapterTest { - @InjectMocks - private SecurityActorAdapter securityActorAdapter; + @Mock + private EmployeeRepository employeeRepository; + + @Mock + private Authentication authentication; @Mock private SecurityContext securityContext; + @InjectMocks + private SecurityActorAdapter securityActorAdapter; + @BeforeEach void setUp() { SecurityContextHolder.setContext(securityContext); } + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + @Test - void currentUser_shouldReturnManagerActor() { - Authentication auth = mock(Authentication.class); - when(auth.isAuthenticated()).thenReturn(true); - when(auth.getName()).thenReturn("managerUser"); - Collection authorities = List.of(new SimpleGrantedAuthority("ROLE_MANAGER")); - doReturn(authorities).when(auth).getAuthorities(); - when(securityContext.getAuthentication()).thenReturn(auth); + void currentUser_whenNotAuthenticated_shouldThrowException() { + when(securityContext.getAuthentication()).thenReturn(null); + + assertThatThrownBy(() -> securityActorAdapter.currentUser()) + .isInstanceOf(NotAuthorizedException.class); + } + + @Test + void currentUser_whenAnonymousUser_shouldThrowException() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn("anonymousUser"); + + assertThatThrownBy(() -> securityActorAdapter.currentUser()) + .isInstanceOf(NotAuthorizedException.class); + } + + @Test + void currentUser_whenEmployeeFoundInRepository_shouldReturnActorFromEmployee() { + String username = "testuser"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + EmployeeEntity employee = new EmployeeEntity(userId, "Test User", username, Role.DOCTOR, Instant.now()); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.of(employee)); Actor actor = securityActorAdapter.currentUser(); - assertThat(actor.role()).isEqualTo(Role.MANAGER); - UUID expectedId = UUID.nameUUIDFromBytes("managerUser".getBytes(StandardCharsets.UTF_8)); - assertThat(actor.userId()).isEqualTo(expectedId); + assertThat(actor.userId()).isEqualTo(userId); + assertThat(actor.role()).isEqualTo(Role.DOCTOR); + assertThat(actor.displayName()).isEqualTo("Test User"); + assertThat(actor.githubUsername()).isEqualTo(username); } @Test - void currentUser_shouldReturnPatientActor_whenNoRoles() { - Authentication auth = mock(Authentication.class); - when(auth.isAuthenticated()).thenReturn(true); - when(auth.getName()).thenReturn("patientUser"); - doReturn(List.of()).when(auth).getAuthorities(); - when(securityContext.getAuthentication()).thenReturn(auth); + void currentUser_whenOAuth2Authentication_shouldUseLoginAttribute() { + String login = "oauth-user"; + UUID userId = UUID.nameUUIDFromBytes(login.getBytes(StandardCharsets.UTF_8)); + + OAuth2AuthenticationToken oauth2Token = mock(OAuth2AuthenticationToken.class); + OAuth2User oauth2User = mock(OAuth2User.class); + + when(securityContext.getAuthentication()).thenReturn(oauth2Token); + when(oauth2Token.isAuthenticated()).thenReturn(true); + when(oauth2Token.getName()).thenReturn("some-other-name"); + when(oauth2Token.getPrincipal()).thenReturn(oauth2User); + when(oauth2User.getAttribute("login")).thenReturn(login); + + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + doReturn(Collections.emptyList()).when(oauth2Token).getAuthorities(); Actor actor = securityActorAdapter.currentUser(); + assertThat(actor.userId()).isEqualTo(userId); + assertThat(actor.githubUsername()).isEqualTo(login); assertThat(actor.role()).isEqualTo(Role.PATIENT); } @Test - void currentUser_shouldThrowException_whenNotAuthenticated() { - Authentication auth = mock(Authentication.class); - when(auth.isAuthenticated()).thenReturn(false); - when(securityContext.getAuthentication()).thenReturn(auth); + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Manager() { + String username = "manager-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_MANAGER"))) + .when(authentication).getAuthorities(); - assertThatThrownBy(() -> securityActorAdapter.currentUser()) - .isInstanceOf(NotAuthorizedException.class); + Actor actor = securityActorAdapter.currentUser(); + + assertThat(actor.role()).isEqualTo(Role.MANAGER); + assertThat(actor.githubUsername()).isEqualTo(username); + assertThat(actor.displayName()).isNull(); } @Test - void currentUser_shouldThrowException_whenAuthIsNull() { - when(securityContext.getAuthentication()).thenReturn(null); + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Doctor() { + String username = "doctor-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_DOCTOR"))) + .when(authentication).getAuthorities(); - assertThatThrownBy(() -> securityActorAdapter.currentUser()) - .isInstanceOf(NotAuthorizedException.class); + Actor actor = securityActorAdapter.currentUser(); + + assertThat(actor.role()).isEqualTo(Role.DOCTOR); + } + + @Test + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Nurse() { + String username = "nurse-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_NURSE"))) + .when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertThat(actor.role()).isEqualTo(Role.NURSE); + } + + @Test + void currentUser_whenEmployeeNotFoundAndNoRoles_shouldDefaultToPatient() { + String username = "patient-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(Collections.emptyList()).when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertThat(actor.role()).isEqualTo(Role.PATIENT); } // Helper because getAuthorities() is wildcard From b3c5af81e8379cae867121f79ec8a902cf954ea8 Mon Sep 17 00:00:00 2001 From: Oscar Nidemar Date: Thu, 9 Apr 2026 09:54:42 +0200 Subject: [PATCH 9/9] Verify parameter forwarding in AuditControllerTest and fix SecurityActorAdapterTest compilation --- .../security/SecurityActorAdapterTest.java | 70 +++---------------- .../rest/AuditControllerTest.java | 11 +++ 2 files changed, 19 insertions(+), 62 deletions(-) diff --git a/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java index 03f9fe0..fe92fee 100644 --- a/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java +++ b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java @@ -3,8 +3,6 @@ import org.example.projektarendehantering.common.Actor; import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.common.Role; -import org.example.projektarendehantering.infrastructure.persistence.EmployeeEntity; -import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -16,14 +14,10 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.OAuth2User; import java.nio.charset.StandardCharsets; -import java.time.Instant; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; @@ -33,9 +27,6 @@ @ExtendWith(MockitoExtension.class) class SecurityActorAdapterTest { - @Mock - private EmployeeRepository employeeRepository; - @Mock private Authentication authentication; @@ -74,57 +65,13 @@ void currentUser_whenAnonymousUser_shouldThrowException() { } @Test - void currentUser_whenEmployeeFoundInRepository_shouldReturnActorFromEmployee() { - String username = "testuser"; - UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); - EmployeeEntity employee = new EmployeeEntity(userId, "Test User", username, Role.DOCTOR, Instant.now()); - - when(securityContext.getAuthentication()).thenReturn(authentication); - when(authentication.isAuthenticated()).thenReturn(true); - when(authentication.getName()).thenReturn(username); - when(employeeRepository.findById(userId)).thenReturn(Optional.of(employee)); - - Actor actor = securityActorAdapter.currentUser(); - - assertThat(actor.userId()).isEqualTo(userId); - assertThat(actor.role()).isEqualTo(Role.DOCTOR); - assertThat(actor.displayName()).isEqualTo("Test User"); - assertThat(actor.githubUsername()).isEqualTo(username); - } - - @Test - void currentUser_whenOAuth2Authentication_shouldUseLoginAttribute() { - String login = "oauth-user"; - UUID userId = UUID.nameUUIDFromBytes(login.getBytes(StandardCharsets.UTF_8)); - - OAuth2AuthenticationToken oauth2Token = mock(OAuth2AuthenticationToken.class); - OAuth2User oauth2User = mock(OAuth2User.class); - - when(securityContext.getAuthentication()).thenReturn(oauth2Token); - when(oauth2Token.isAuthenticated()).thenReturn(true); - when(oauth2Token.getName()).thenReturn("some-other-name"); - when(oauth2Token.getPrincipal()).thenReturn(oauth2User); - when(oauth2User.getAttribute("login")).thenReturn(login); - - when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); - doReturn(Collections.emptyList()).when(oauth2Token).getAuthorities(); - - Actor actor = securityActorAdapter.currentUser(); - - assertThat(actor.userId()).isEqualTo(userId); - assertThat(actor.githubUsername()).isEqualTo(login); - assertThat(actor.role()).isEqualTo(Role.PATIENT); - } - - @Test - void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Manager() { + void currentUser_shouldReturnActorWithManagerRole() { String username = "manager-user"; UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.isAuthenticated()).thenReturn(true); when(authentication.getName()).thenReturn(username); - when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); doReturn(List.of(new SimpleGrantedAuthority("ROLE_MANAGER"))) .when(authentication).getAuthorities(); @@ -132,19 +79,17 @@ void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Manager() { Actor actor = securityActorAdapter.currentUser(); assertThat(actor.role()).isEqualTo(Role.MANAGER); - assertThat(actor.githubUsername()).isEqualTo(username); - assertThat(actor.displayName()).isNull(); + assertThat(actor.userId()).isEqualTo(userId); } @Test - void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Doctor() { + void currentUser_shouldReturnActorWithDoctorRole() { String username = "doctor-user"; UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.isAuthenticated()).thenReturn(true); when(authentication.getName()).thenReturn(username); - when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); doReturn(List.of(new SimpleGrantedAuthority("ROLE_DOCTOR"))) .when(authentication).getAuthorities(); @@ -152,17 +97,17 @@ void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Doctor() { Actor actor = securityActorAdapter.currentUser(); assertThat(actor.role()).isEqualTo(Role.DOCTOR); + assertThat(actor.userId()).isEqualTo(userId); } @Test - void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Nurse() { + void currentUser_shouldReturnActorWithNurseRole() { String username = "nurse-user"; UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.isAuthenticated()).thenReturn(true); when(authentication.getName()).thenReturn(username); - when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); doReturn(List.of(new SimpleGrantedAuthority("ROLE_NURSE"))) .when(authentication).getAuthorities(); @@ -170,23 +115,24 @@ void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Nurse() { Actor actor = securityActorAdapter.currentUser(); assertThat(actor.role()).isEqualTo(Role.NURSE); + assertThat(actor.userId()).isEqualTo(userId); } @Test - void currentUser_whenEmployeeNotFoundAndNoRoles_shouldDefaultToPatient() { + void currentUser_whenNoRoles_shouldDefaultToPatient() { String username = "patient-user"; UUID userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)); when(securityContext.getAuthentication()).thenReturn(authentication); when(authentication.isAuthenticated()).thenReturn(true); when(authentication.getName()).thenReturn(username); - when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); doReturn(Collections.emptyList()).when(authentication).getAuthorities(); Actor actor = securityActorAdapter.currentUser(); assertThat(actor.role()).isEqualTo(Role.PATIENT); + assertThat(actor.userId()).isEqualTo(userId); } // Helper because getAuthorities() is wildcard diff --git a/src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java b/src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java index d13b807..bea5285 100644 --- a/src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java +++ b/src/test/java/org/example/projektarendehantering/presentation/rest/AuditControllerTest.java @@ -21,7 +21,10 @@ import java.util.UUID; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -84,6 +87,14 @@ void list_withFilters_shouldPassParameters() throws Exception { .param("page", "1") .param("size", "10")) .andExpect(status().isOk()); + + verify(auditService).listEvents( + eq(managerActor), + isNull(), + isNull(), + eq(caseId), + argThat(p -> p.getPageNumber() == 1 && p.getPageSize() == 10) + ); } @Test