Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,3 @@ public EmployeeEntity(UUID id, String displayName, String githubUsername, Role r
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}

Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,3 @@ public EmployeeDTO(UUID id, String displayName, String githubUsername, Role role
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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.Comparator;
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<AuditEventEntity> events = auditEventRepository.findAll();
assertThat(events.size()).isGreaterThan((int) countBefore);

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");
Comment thread
Tyreviel marked this conversation as resolved.
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);

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")
.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);

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\"")
.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

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);
}
}
Original file line number Diff line number Diff line change
@@ -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, "Manager", "manager");
doctorActor = new Actor(UUID.randomUUID(), Role.DOCTOR, "Doctor", "doctor");
caseId = UUID.randomUUID();
}

@Test
void listEvents_shouldAllowManagerToSeeAll() {
Page<AuditEventEntity> page = new PageImpl<>(List.of(new AuditEventEntity()));
when(auditEventRepository.findAllByOccurredAtBetweenOrderByOccurredAtDesc(any(), any(), any()))
.thenReturn(page);
when(auditEventMapper.toDTO(any())).thenReturn(new AuditEventDTO());

Page<AuditEventDTO> 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<AuditEventDTO> 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, "Patient", "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");
}
}
Original file line number Diff line number Diff line change
@@ -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, "Doctor", "doctor_user");
nurseActor = new Actor(nurseId, Role.NURSE, "Nurse", "nurse_user");
managerActor = new Actor(managerId, Role.MANAGER, "Manager", "manager_user");
patientActor = new Actor(patientId, Role.PATIENT, "Patient", "patient_user");

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, "Unauthorized", "unauthorized_user");
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);
}
}
Loading