refactor(stats): introduce StatsService and require READ_ALL

StatsController previously injected PersonRepository and DocumentRepository
directly, violating the controller→service→repository layering rule. Move the
two count() calls into a thin StatsService that delegates to PersonService.count
and DocumentService.count. While here, add the missing @RequirePermission(READ_ALL)
flagged by AUDIT-2 §7 — anonymous callers were able to read aggregate document/
person counts.

Refs #417 (C6.1 violation #1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-04 22:20:14 +02:00
parent eedf5e3ac1
commit d5e0e969ef
6 changed files with 86 additions and 16 deletions

View File

@@ -2,10 +2,10 @@ package org.raddatz.familienarchiv.controller;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.config.SecurityConfig;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.repository.PersonRepository;
import org.raddatz.familienarchiv.dto.StatsDTO;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.service.CustomUserDetailsService;
import org.raddatz.familienarchiv.service.StatsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
@@ -25,8 +25,7 @@ class StatsControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean PersonRepository personRepository;
@MockitoBean DocumentRepository documentRepository;
@MockitoBean StatsService statsService;
@MockitoBean CustomUserDetailsService customUserDetailsService;
@Test
@@ -37,9 +36,15 @@ class StatsControllerTest {
@Test
@WithMockUser
void getStats_returns403_whenUserLacksReadAll() throws Exception {
mockMvc.perform(get("/api/stats"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "READ_ALL")
void getStats_returns200_withCorrectCounts() throws Exception {
when(personRepository.count()).thenReturn(4L);
when(documentRepository.count()).thenReturn(12L);
when(statsService.getStats()).thenReturn(new StatsDTO(4L, 12L));
mockMvc.perform(get("/api/stats"))
.andExpect(status().isOk())
@@ -48,10 +53,9 @@ class StatsControllerTest {
}
@Test
@WithMockUser
@WithMockUser(authorities = "READ_ALL")
void getStats_returns200_withZeroCounts() throws Exception {
when(personRepository.count()).thenReturn(0L);
when(documentRepository.count()).thenReturn(0L);
when(statsService.getStats()).thenReturn(new StatsDTO(0L, 0L));
mockMvc.perform(get("/api/stats"))
.andExpect(status().isOk())

View File

@@ -0,0 +1,41 @@
package org.raddatz.familienarchiv.service;
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.raddatz.familienarchiv.dto.StatsDTO;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class StatsServiceTest {
@Mock PersonService personService;
@Mock DocumentService documentService;
@InjectMocks StatsService statsService;
@Test
void getStats_returnsCountsFromServices() {
when(personService.count()).thenReturn(4L);
when(documentService.count()).thenReturn(12L);
StatsDTO stats = statsService.getStats();
assertThat(stats.totalPersons()).isEqualTo(4L);
assertThat(stats.totalDocuments()).isEqualTo(12L);
}
@Test
void getStats_returnsZero_whenNoEntities() {
when(personService.count()).thenReturn(0L);
when(documentService.count()).thenReturn(0L);
StatsDTO stats = statsService.getStats();
assertThat(stats.totalPersons()).isZero();
assertThat(stats.totalDocuments()).isZero();
}
}