feat(dashboard): add kinds param to GET /api/dashboard/activity

Spring auto-converts ?kinds=FILE_UPLOADED,TEXT_SAVED to Set<AuditKind>.
Absent or empty kinds defaults to ROLLUP_ELIGIBLE. Unknown value → 400.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-21 21:10:43 +02:00
parent 571ecfc626
commit 8d16e4d975
4 changed files with 78 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
package org.raddatz.familienarchiv.dashboard; package org.raddatz.familienarchiv.dashboard;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.Permission;
import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.security.RequirePermission;
import org.raddatz.familienarchiv.security.SecurityUtils; import org.raddatz.familienarchiv.security.SecurityUtils;
@@ -9,6 +10,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@@ -35,8 +37,10 @@ public class DashboardController {
@GetMapping("/activity") @GetMapping("/activity")
public List<ActivityFeedItemDTO> getActivity( public List<ActivityFeedItemDTO> getActivity(
Authentication authentication, Authentication authentication,
@RequestParam(defaultValue = "7") int limit) { @RequestParam(defaultValue = "7") int limit,
@RequestParam(required = false) Set<AuditKind> kinds) {
UUID userId = SecurityUtils.requireUserId(authentication, userService); UUID userId = SecurityUtils.requireUserId(authentication, userService);
return dashboardService.getActivity(userId, Math.min(limit, 40)); Set<AuditKind> effectiveKinds = (kinds == null || kinds.isEmpty()) ? AuditKind.ROLLUP_ELIGIBLE : kinds;
return dashboardService.getActivity(userId, Math.min(limit, 40), effectiveKinds);
} }
} }

View File

@@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.audit.ActivityActorDTO; import org.raddatz.familienarchiv.audit.ActivityActorDTO;
import org.raddatz.familienarchiv.audit.ActivityFeedRow; import org.raddatz.familienarchiv.audit.ActivityFeedRow;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditLogQueryService; import org.raddatz.familienarchiv.audit.AuditLogQueryService;
import org.raddatz.familienarchiv.audit.PulseStatsRow; import org.raddatz.familienarchiv.audit.PulseStatsRow;
import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.AppUser;
@@ -110,8 +111,8 @@ public class DashboardService {
); );
} }
public List<ActivityFeedItemDTO> getActivity(UUID currentUserId, int limit) { public List<ActivityFeedItemDTO> getActivity(UUID currentUserId, int limit, Set<AuditKind> kinds) {
List<ActivityFeedRow> rows = auditLogQueryService.findActivityFeed(currentUserId, limit); List<ActivityFeedRow> rows = auditLogQueryService.findActivityFeed(currentUserId, limit, kinds);
List<UUID> docIds = rows.stream() List<UUID> docIds = rows.stream()
.map(ActivityFeedRow::getDocumentId) .map(ActivityFeedRow::getDocumentId)

View File

@@ -1,6 +1,7 @@
package org.raddatz.familienarchiv.dashboard; package org.raddatz.familienarchiv.dashboard;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.config.SecurityConfig; import org.raddatz.familienarchiv.config.SecurityConfig;
import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.AppUser;
import org.raddatz.familienarchiv.security.PermissionAspect; import org.raddatz.familienarchiv.security.PermissionAspect;
@@ -15,10 +16,12 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 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.jsonPath;
@@ -134,7 +137,7 @@ class DashboardControllerTest {
UUID userId = UUID.randomUUID(); UUID userId = UUID.randomUUID();
when(userService.findByEmail(any())).thenReturn( when(userService.findByEmail(any())).thenReturn(
AppUser.builder().id(userId).email("u@test.com").password("pw").build()); AppUser.builder().id(userId).email("u@test.com").password("pw").build());
when(dashboardService.getActivity(any(UUID.class), anyInt())).thenReturn(List.of()); when(dashboardService.getActivity(any(UUID.class), anyInt(), any(Set.class))).thenReturn(List.of());
mockMvc.perform(get("/api/dashboard/activity")) mockMvc.perform(get("/api/dashboard/activity"))
.andExpect(status().isOk()) .andExpect(status().isOk())
@@ -147,11 +150,66 @@ class DashboardControllerTest {
UUID userId = UUID.randomUUID(); UUID userId = UUID.randomUUID();
when(userService.findByEmail(any())).thenReturn( when(userService.findByEmail(any())).thenReturn(
AppUser.builder().id(userId).email("u@test.com").password("pw").build()); AppUser.builder().id(userId).email("u@test.com").password("pw").build());
when(dashboardService.getActivity(any(UUID.class), anyInt())).thenReturn(List.of()); when(dashboardService.getActivity(any(UUID.class), anyInt(), any(Set.class))).thenReturn(List.of());
mockMvc.perform(get("/api/dashboard/activity").param("limit", "9999")) mockMvc.perform(get("/api/dashboard/activity").param("limit", "9999"))
.andExpect(status().isOk()); .andExpect(status().isOk());
org.mockito.Mockito.verify(dashboardService).getActivity(any(UUID.class), org.mockito.ArgumentMatchers.eq(40)); verify(dashboardService).getActivity(any(UUID.class), org.mockito.ArgumentMatchers.eq(40), any(Set.class));
}
// ─── GET /api/dashboard/activity — kinds param ───────────────────────────
@Test
@WithMockUser(authorities = "READ_ALL")
void activity_parsesKinds_fromCsvQueryParam() 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(), any(Set.class))).thenReturn(List.of());
mockMvc.perform(get("/api/dashboard/activity")
.param("kinds", "FILE_UPLOADED", "TEXT_SAVED"))
.andExpect(status().isOk());
verify(dashboardService).getActivity(any(UUID.class), anyInt(),
org.mockito.ArgumentMatchers.eq(Set.of(AuditKind.FILE_UPLOADED, AuditKind.TEXT_SAVED)));
}
@Test
@WithMockUser(authorities = "READ_ALL")
void activity_returns400_forUnknownKindValue() throws Exception {
mockMvc.perform(get("/api/dashboard/activity").param("kinds", "INVALID_KIND"))
.andExpect(status().isBadRequest());
}
@Test
@WithMockUser(authorities = "READ_ALL")
void activity_defaults_to_rollupEligible_whenKindsAbsent() 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(), any(Set.class))).thenReturn(List.of());
mockMvc.perform(get("/api/dashboard/activity"))
.andExpect(status().isOk());
verify(dashboardService).getActivity(any(UUID.class), anyInt(),
org.mockito.ArgumentMatchers.eq(AuditKind.ROLLUP_ELIGIBLE));
}
@Test
@WithMockUser(authorities = "READ_ALL")
void activity_treats_single_valid_kind_as_filter() 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(), any(Set.class))).thenReturn(List.of());
mockMvc.perform(get("/api/dashboard/activity").param("kinds", "COMMENT_ADDED"))
.andExpect(status().isOk());
verify(dashboardService).getActivity(any(UUID.class), anyInt(),
org.mockito.ArgumentMatchers.eq(Set.of(AuditKind.COMMENT_ADDED)));
} }
} }

View File

@@ -88,7 +88,7 @@ class DashboardServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
ActivityFeedRow row = mockFeedRow(docId, "ANNOTATION_CREATED"); ActivityFeedRow row = mockFeedRow(docId, "ANNOTATION_CREATED");
when(auditLogQueryService.findActivityFeed(userId, 5)).thenReturn(List.of(row, row)); when(auditLogQueryService.findActivityFeed(userId, 5, AuditKind.ROLLUP_ELIGIBLE)).thenReturn(List.of(row, row));
Document doc = Document.builder() Document doc = Document.builder()
.id(docId).title("Familienbrief").originalFilename("f.pdf") .id(docId).title("Familienbrief").originalFilename("f.pdf")
@@ -96,7 +96,7 @@ class DashboardServiceTest {
.build(); .build();
when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of(doc)); when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of(doc));
List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5); List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE);
assertThat(items).hasSize(2); assertThat(items).hasSize(2);
assertThat(items.get(0).documentTitle()).isEqualTo("Familienbrief"); assertThat(items.get(0).documentTitle()).isEqualTo("Familienbrief");
@@ -112,13 +112,13 @@ class DashboardServiceTest {
UUID commentId = UUID.randomUUID(); UUID commentId = UUID.randomUUID();
ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", commentId); ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", commentId);
when(auditLogQueryService.findActivityFeed(userId, 5)).thenReturn(List.of(row)); when(auditLogQueryService.findActivityFeed(userId, 5, AuditKind.ROLLUP_ELIGIBLE)).thenReturn(List.of(row));
when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of( when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of(
Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build() Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build()
)); ));
when(commentService.findAnnotationIdsByIds(List.of(commentId))).thenReturn(Map.of()); when(commentService.findAnnotationIdsByIds(List.of(commentId))).thenReturn(Map.of());
List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5); List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE);
assertThat(items).hasSize(1); assertThat(items).hasSize(1);
assertThat(items.get(0).commentId()).isEqualTo(commentId); assertThat(items.get(0).commentId()).isEqualTo(commentId);
@@ -132,14 +132,14 @@ class DashboardServiceTest {
UUID annotationId = UUID.randomUUID(); UUID annotationId = UUID.randomUUID();
ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", commentId); ActivityFeedRow row = mockFeedRow(docId, "COMMENT_ADDED", commentId);
when(auditLogQueryService.findActivityFeed(userId, 5)).thenReturn(List.of(row)); when(auditLogQueryService.findActivityFeed(userId, 5, AuditKind.ROLLUP_ELIGIBLE)).thenReturn(List.of(row));
when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of( when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of(
Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build() Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build()
)); ));
when(commentService.findAnnotationIdsByIds(List.of(commentId))) when(commentService.findAnnotationIdsByIds(List.of(commentId)))
.thenReturn(Map.of(commentId, annotationId)); .thenReturn(Map.of(commentId, annotationId));
List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5); List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE);
assertThat(items).hasSize(1); assertThat(items).hasSize(1);
assertThat(items.get(0).annotationId()).isEqualTo(annotationId); assertThat(items.get(0).annotationId()).isEqualTo(annotationId);
@@ -151,12 +151,12 @@ class DashboardServiceTest {
UUID docId = UUID.randomUUID(); UUID docId = UUID.randomUUID();
ActivityFeedRow row = mockFeedRow(docId, "TEXT_SAVED", null); ActivityFeedRow row = mockFeedRow(docId, "TEXT_SAVED", null);
when(auditLogQueryService.findActivityFeed(userId, 5)).thenReturn(List.of(row)); when(auditLogQueryService.findActivityFeed(userId, 5, AuditKind.ROLLUP_ELIGIBLE)).thenReturn(List.of(row));
when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of( when(documentService.getDocumentsByIds(List.of(docId))).thenReturn(List.of(
Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build() Document.builder().id(docId).title("B").originalFilename("b.pdf").receivers(new HashSet<>()).build()
)); ));
List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5); List<ActivityFeedItemDTO> items = dashboardService.getActivity(userId, 5, AuditKind.ROLLUP_ELIGIBLE);
assertThat(items).hasSize(1); assertThat(items).hasSize(1);
assertThat(items.get(0).commentId()).isNull(); assertThat(items.get(0).commentId()).isNull();