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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-29 11:29:34 +02:00
parent dc487e2f97
commit 6976daa910
4 changed files with 58 additions and 2 deletions

View File

@@ -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<IncompleteDocumentDTO> 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<List<Document>> getRecentActivity(
@RequestParam(defaultValue = "5") int size) {
return ResponseEntity.ok(documentService.getRecentActivity(size));
}
@GetMapping("/search")
public ResponseEntity<List<Document>> 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<String> 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));
}

View File

@@ -260,6 +260,12 @@ public class DocumentService {
return documentRepository.save(doc);
}
// 0. Zuletzt aktive Dokumente (sortiert nach updatedAt DESC)
public List<Document> 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<Document> searchDocuments(String text, LocalDate from, LocalDate to, UUID sender, UUID receiver, List<String> tags, DocumentStatus status) {
Specification<Document> spec = Specification.where(hasText(text))

View File

@@ -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

View File

@@ -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<Document> result = documentService.getRecentActivity(2);
assertThat(result).hasSize(2);
assertThat(result).containsExactly(doc3, doc2);
}
}