From 51479733798a133bff883074016c8bee9dd71228 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 19 Apr 2026 21:27:07 +0200 Subject: [PATCH] refactor(dashboard): remove page field from DashboardResumeDTO; rename pages to totalBlocks Co-Authored-By: Claude Sonnet 4.6 --- .../dashboard/DashboardResumeDTO.java | 18 +++ .../dashboard/DashboardService.java | 12 +- .../dashboard/DashboardControllerTest.java | 130 ++++++++++++++++++ 3 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardResumeDTO.java create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardControllerTest.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardResumeDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardResumeDTO.java new file mode 100644 index 00000000..f7fa95ed --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardResumeDTO.java @@ -0,0 +1,18 @@ +package org.raddatz.familienarchiv.dashboard; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; + +import java.util.List; +import java.util.UUID; + +public record DashboardResumeDTO( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID documentId, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String title, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String caption, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String excerpt, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int totalBlocks, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) int pct, + @Nullable String thumbnailUrl, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) List collaborators +) {} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java index ded30f76..8734c14a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dashboard/DashboardService.java @@ -53,16 +53,6 @@ public class DashboardService { long reviewedBlocks = blocks.stream().filter(TranscriptionBlock::isReviewed).count(); int pct = totalBlocks > 0 ? (int) (reviewedBlocks * 100L / totalBlocks) : 0; - Set pageNumbers = new HashSet<>(); - for (TranscriptionBlock b : blocks) { - pageNumbers.add(b.getSortOrder()); - } - int maxPage = blocks.stream().mapToInt(TranscriptionBlock::getSortOrder).max().orElse(1); - int totalPages = Math.max(1, (int) blocks.stream() - .mapToInt(TranscriptionBlock::getSortOrder) - .distinct().count()); - int currentPage = 1; - String caption = buildCaption(doc); List collaboratorIds = blocks.stream() @@ -85,7 +75,7 @@ public class DashboardService { .toList(); return new DashboardResumeDTO(docId, doc.getTitle(), caption, excerpt, - currentPage, totalPages, pct, null, collaborators); + totalBlocks, pct, null, collaborators); } public DashboardPulseDTO getPulse(UUID userId) { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardControllerTest.java new file mode 100644 index 00000000..2c76d683 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/dashboard/DashboardControllerTest.java @@ -0,0 +1,130 @@ +package org.raddatz.familienarchiv.dashboard; + +import org.junit.jupiter.api.Test; +import org.raddatz.familienarchiv.config.SecurityConfig; +import org.raddatz.familienarchiv.model.AppUser; +import org.raddatz.familienarchiv.security.PermissionAspect; +import org.raddatz.familienarchiv.service.CustomUserDetailsService; +import org.raddatz.familienarchiv.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Import; +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 java.util.List; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; +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; + +@WebMvcTest(DashboardController.class) +@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) +class DashboardControllerTest { + + @Autowired MockMvc mockMvc; + + @MockitoBean DashboardService dashboardService; + @MockitoBean UserService userService; + @MockitoBean CustomUserDetailsService customUserDetailsService; + + // ─── Security ──────────────────────────────────────────────────────────────── + + @Test + void resume_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(get("/api/dashboard/resume")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void resume_returns403_whenUserHasNoPermissions() throws Exception { + mockMvc.perform(get("/api/dashboard/resume")) + .andExpect(status().isForbidden()); + } + + @Test + void pulse_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(get("/api/dashboard/pulse")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void pulse_returns403_whenUserHasNoPermissions() throws Exception { + mockMvc.perform(get("/api/dashboard/pulse")) + .andExpect(status().isForbidden()); + } + + // ─── GET /api/dashboard/resume ──────────────────────────────────────────── + + @Test + @WithMockUser(authorities = "READ_ALL") + void resume_returnsNull_whenNoInProgressTranscription() throws Exception { + UUID userId = UUID.randomUUID(); + when(userService.findByEmail(any())).thenReturn( + AppUser.builder().id(userId).email("u@test.com").password("pw").build()); + when(dashboardService.getResume(userId)).thenReturn(null); + + mockMvc.perform(get("/api/dashboard/resume")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").doesNotExist()); + } + + @Test + @WithMockUser(authorities = "READ_ALL") + void resume_returnsDocument_whenUserHasInProgressTranscription() throws Exception { + UUID userId = UUID.randomUUID(); + UUID docId = UUID.randomUUID(); + when(userService.findByEmail(any())).thenReturn( + AppUser.builder().id(userId).email("u@test.com").password("pw").build()); + + DashboardResumeDTO resume = new DashboardResumeDTO(docId, "Brief an Frieda", + "Frieda an Wilhelm · 1923", "Liebe Frieda…", 4, 38, null, List.of()); + when(dashboardService.getResume(userId)).thenReturn(resume); + + mockMvc.perform(get("/api/dashboard/resume")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.documentId").value(docId.toString())) + .andExpect(jsonPath("$.pct").value(38)); + } + + // ─── GET /api/dashboard/pulse ───────────────────────────────────────────── + + @Test + @WithMockUser(authorities = "READ_ALL") + void pulse_returnsWeekStats() throws Exception { + UUID userId = UUID.randomUUID(); + when(userService.findByEmail(any())).thenReturn( + AppUser.builder().id(userId).email("u@test.com").password("pw").build()); + + DashboardPulseDTO pulse = new DashboardPulseDTO(86, 23, 9, 47, 4, List.of()); + when(dashboardService.getPulse(userId)).thenReturn(pulse); + + mockMvc.perform(get("/api/dashboard/pulse")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.pages").value(86)) + .andExpect(jsonPath("$.annotated").value(23)); + } + + // ─── GET /api/dashboard/activity ───────────────────────────────────────── + + @Test + @WithMockUser(authorities = "READ_ALL") + void activity_returnsDeduplicated_feed() throws Exception { + UUID userId = UUID.randomUUID(); + when(userService.findByEmail(any())).thenReturn( + AppUser.builder().id(userId).email("u@test.com").password("pw").build()); + when(dashboardService.getActivity(any(UUID.class), anyInt())).thenReturn(List.of()); + + mockMvc.perform(get("/api/dashboard/activity")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()); + } +}