From 6976daa910a4e963931d5b3f9b709674ae1ead91 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 29 Mar 2026 11:29:34 +0200 Subject: [PATCH] feat(#145): add recent-activity endpoint sorted by updatedAt Add GET /api/documents/recent-activity?size=N endpoint that returns the N most recently updated documents sorted by updatedAt DESC. Includes TDD: failing tests written first, then production code. Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 11 ++++++++-- .../service/DocumentService.java | 6 +++++ .../controller/DocumentControllerTest.java | 21 ++++++++++++++++++ .../service/DocumentServiceTest.java | 22 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index 90ab0bd0..7b49a148 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -11,6 +11,7 @@ import java.util.UUID; +import io.swagger.v3.oas.annotations.Parameter; import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; import org.raddatz.familienarchiv.dto.DocumentVersionSummary; import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO; @@ -167,7 +168,7 @@ public class DocumentController { @GetMapping("/incomplete") public List getIncomplete( - @RequestParam(defaultValue = "10") int size) { + @Parameter(description = "Maximum number of results") @RequestParam(defaultValue = "10") int size) { return documentService.findIncompleteDocuments(size); } @@ -178,6 +179,12 @@ public class DocumentController { .orElse(ResponseEntity.noContent().build()); } + @GetMapping("/recent-activity") + public ResponseEntity> getRecentActivity( + @RequestParam(defaultValue = "5") int size) { + return ResponseEntity.ok(documentService.getRecentActivity(size)); + } + @GetMapping("/search") public ResponseEntity> search( @RequestParam(required = false) String q, @@ -186,7 +193,7 @@ public class DocumentController { @RequestParam(required = false) UUID senderId, @RequestParam(required = false) UUID receiverId, @RequestParam(required = false, name = "tag") List tags, - @RequestParam(required = false) DocumentStatus status) { + @Parameter(description = "Filter by document status") @RequestParam(required = false) DocumentStatus status) { return ResponseEntity.ok(documentService.searchDocuments(q, from, to, senderId, receiverId, tags, status)); } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index ae0e360d..eae924da 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -260,6 +260,12 @@ public class DocumentService { return documentRepository.save(doc); } + // 0. Zuletzt aktive Dokumente (sortiert nach updatedAt DESC) + public List getRecentActivity(int size) { + return documentRepository.findAll(Sort.by(Sort.Direction.DESC, "updatedAt")) + .stream().limit(size).toList(); + } + // 1. Allgemeine Suche (für das Suchfeld im Frontend) public List searchDocuments(String text, LocalDate from, LocalDate to, UUID sender, UUID receiver, List tags, DocumentStatus status) { Specification spec = Specification.where(hasText(text)) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java index 94284d48..47a384a7 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -406,6 +406,27 @@ class DocumentControllerTest { .andExpect(status().isNoContent()); } + // ─── GET /api/documents/recent-activity ────────────────────────────────── + + @Test + void getRecentActivity_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(get("/api/documents/recent-activity")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void getRecentActivity_returnsOkWithDocuments() throws Exception { + Document doc1 = Document.builder().id(UUID.randomUUID()).title("Alpha").originalFilename("a.pdf").build(); + Document doc2 = Document.builder().id(UUID.randomUUID()).title("Beta").originalFilename("b.pdf").build(); + when(documentService.getRecentActivity(5)).thenReturn(List.of(doc1, doc2)); + + mockMvc.perform(get("/api/documents/recent-activity").param("size", "5")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].title").value("Alpha")) + .andExpect(jsonPath("$[1].title").value("Beta")); + } + // ─── GET /api/documents/{id}/versions ──────────────────────────────────── @Test diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java index 80910432..0eed78a1 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java @@ -1213,4 +1213,26 @@ class DocumentServiceTest { verify(documentRepository).findAll(any(org.springframework.data.jpa.domain.Specification.class), any(Sort.class)); } + + // ─── getRecentActivity ──────────────────────────────────────────────────── + + @Test + void getRecentActivity_returnsMostRecentlyUpdatedDocuments() { + java.time.LocalDateTime oldest = java.time.LocalDateTime.of(2024, 1, 1, 0, 0); + java.time.LocalDateTime middle = java.time.LocalDateTime.of(2024, 6, 1, 0, 0); + java.time.LocalDateTime newest = java.time.LocalDateTime.of(2024, 12, 1, 0, 0); + + Document doc1 = Document.builder().id(UUID.randomUUID()).title("Oldest").build(); + Document doc2 = Document.builder().id(UUID.randomUUID()).title("Middle").build(); + Document doc3 = Document.builder().id(UUID.randomUUID()).title("Newest").build(); + + // findAll(Sort) returns documents already sorted DESC by updatedAt + when(documentRepository.findAll(Sort.by(Sort.Direction.DESC, "updatedAt"))) + .thenReturn(List.of(doc3, doc2, doc1)); + + List result = documentService.getRecentActivity(2); + + assertThat(result).hasSize(2); + assertThat(result).containsExactly(doc3, doc2); + } }