From 28256dbd08b2b4be83d877faae6c126f4c8234e6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 23 Mar 2026 11:30:05 +0100 Subject: [PATCH] feat: wire document versioning into DocumentService and DocumentController DocumentService now calls documentVersionService.recordVersion() after createDocument and updateDocument. DocumentController exposes two new read-only endpoints: GET /{id}/versions and GET /{id}/versions/{versionId}. Refs #38 Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 18 +++++++ .../service/DocumentService.java | 9 +++- .../controller/DocumentControllerTest.java | 51 +++++++++++++++++++ .../service/DocumentServiceTest.java | 35 +++++++++++++ 4 files changed, 111 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 a4b30d81..f23f465b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -5,13 +5,18 @@ import java.time.LocalDate; import java.util.List; import java.util.UUID; + + import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; +import org.raddatz.familienarchiv.dto.DocumentVersionSummary; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.Document; +import org.raddatz.familienarchiv.model.DocumentVersion; import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.service.DocumentService; +import org.raddatz.familienarchiv.service.DocumentVersionService; import org.raddatz.familienarchiv.service.FileService; import org.springframework.data.domain.Sort; import org.springframework.http.HttpHeaders; @@ -39,6 +44,7 @@ import lombok.extern.slf4j.Slf4j; public class DocumentController { private final DocumentService documentService; + private final DocumentVersionService documentVersionService; private final FileService fileService; // --- DOWNLOAD --- @@ -108,6 +114,18 @@ public class DocumentController { return ResponseEntity.ok(documentService.searchDocuments(q, from, to, senderId, receiverId, tags)); } + // --- VERSIONS --- + + @GetMapping("/{id}/versions") + public List getVersions(@PathVariable UUID id) { + return documentVersionService.getSummaries(id); + } + + @GetMapping("/{id}/versions/{versionId}") + public DocumentVersion getVersion(@PathVariable UUID id, @PathVariable UUID versionId) { + return documentVersionService.getVersion(id, versionId); + } + @GetMapping("/conversation") public List getConversation( @RequestParam UUID senderId, 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 2c8f5ce1..303fda53 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -37,6 +37,7 @@ public class DocumentService { private final PersonService personService; private final FileService fileService; private final TagService tagService; + private final DocumentVersionService documentVersionService; /** * Lädt eine Datei hoch. @@ -125,7 +126,9 @@ public class DocumentService { doc.setStatus(DocumentStatus.UPLOADED); } - return documentRepository.save(doc); + Document finalDoc = documentRepository.save(doc); + documentVersionService.recordVersion(finalDoc); + return finalDoc; } @Transactional @@ -178,7 +181,9 @@ public class DocumentService { doc.setStatus(DocumentStatus.UPLOADED); } - return documentRepository.save(doc); + Document saved = documentRepository.save(doc); + documentVersionService.recordVersion(saved); + return saved; } public Document updateDocumentTags(UUID docId, List tagNames) { 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 a560d4d3..545e98f3 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -1,10 +1,13 @@ package org.raddatz.familienarchiv.controller; import org.junit.jupiter.api.Test; +import org.raddatz.familienarchiv.dto.DocumentVersionSummary; import org.raddatz.familienarchiv.model.Document; +import org.raddatz.familienarchiv.model.DocumentVersion; import org.raddatz.familienarchiv.security.PermissionAspect; import org.raddatz.familienarchiv.service.CustomUserDetailsService; import org.raddatz.familienarchiv.service.DocumentService; +import org.raddatz.familienarchiv.service.DocumentVersionService; import org.raddatz.familienarchiv.service.FileService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; @@ -15,13 +18,16 @@ 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.time.LocalDateTime; import java.util.Collections; +import java.util.List; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; 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.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(DocumentController.class) @@ -31,6 +37,7 @@ class DocumentControllerTest { @Autowired MockMvc mockMvc; @MockitoBean DocumentService documentService; + @MockitoBean DocumentVersionService documentVersionService; @MockitoBean FileService fileService; @MockitoBean CustomUserDetailsService customUserDetailsService; @@ -113,4 +120,48 @@ class DocumentControllerTest { .with(req -> { req.setMethod("PUT"); return req; })) .andExpect(status().isOk()); } + + // ─── GET /api/documents/{id}/versions ──────────────────────────────────── + + @Test + void getVersions_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(get("/api/documents/" + UUID.randomUUID() + "/versions")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void getVersions_returns200_whenAuthenticated() throws Exception { + UUID docId = UUID.randomUUID(); + DocumentVersionSummary summary = new DocumentVersionSummary( + UUID.randomUUID(), LocalDateTime.now(), "Emma Müller", List.of("title")); + when(documentVersionService.getSummaries(docId)).thenReturn(List.of(summary)); + + mockMvc.perform(get("/api/documents/" + docId + "/versions")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].editorName").value("Emma Müller")); + } + + // ─── GET /api/documents/{id}/versions/{versionId} ──────────────────────── + + @Test + void getVersion_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(get("/api/documents/" + UUID.randomUUID() + "/versions/" + UUID.randomUUID())) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void getVersion_returns200_whenAuthenticated() throws Exception { + UUID docId = UUID.randomUUID(); + UUID versionId = UUID.randomUUID(); + DocumentVersion version = DocumentVersion.builder() + .id(versionId).documentId(docId).savedAt(LocalDateTime.now()) + .editorName("Otto").snapshot("{\"title\":\"Brief\"}").changedFields("[]").build(); + when(documentVersionService.getVersion(docId, versionId)).thenReturn(version); + + mockMvc.perform(get("/api/documents/" + docId + "/versions/" + versionId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.editorName").value("Otto")); + } } 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 d3ff6cdd..e531bc44 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java @@ -8,6 +8,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.Document; +import org.raddatz.familienarchiv.model.DocumentStatus; import org.raddatz.familienarchiv.model.Tag; import org.raddatz.familienarchiv.repository.DocumentRepository; @@ -29,6 +30,7 @@ class DocumentServiceTest { @Mock PersonService personService; @Mock FileService fileService; @Mock TagService tagService; + @Mock DocumentVersionService documentVersionService; @InjectMocks DocumentService documentService; // ─── getDocumentById ────────────────────────────────────────────────────── @@ -132,4 +134,37 @@ class DocumentServiceTest { assertThat(documentService.getDocumentsByReceiver(receiverId)).containsExactly(doc); } + + // ─── versioning ─────────────────────────────────────────────────────────── + + @Test + void createDocument_recordsVersionAfterSave() throws Exception { + DocumentUpdateDTO dto = new DocumentUpdateDTO(); + dto.setTitle("Neuer Brief"); + + Document saved = Document.builder() + .id(UUID.randomUUID()).title("Neuer Brief") + .originalFilename("Neuer Brief").status(DocumentStatus.PLACEHOLDER) + .build(); + when(documentRepository.save(any())).thenReturn(saved); + when(documentRepository.findById(any())).thenReturn(Optional.of(saved)); + + documentService.createDocument(dto, null); + + verify(documentVersionService, atLeastOnce()).recordVersion(any(Document.class)); + } + + @Test + void updateDocument_recordsVersionAfterSave() throws Exception { + UUID id = UUID.randomUUID(); + Document existing = Document.builder() + .id(id).title("Alt").originalFilename("alt.pdf") + .status(DocumentStatus.PLACEHOLDER).build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(existing)); + when(documentRepository.save(any())).thenReturn(existing); + + documentService.updateDocument(id, new DocumentUpdateDTO(), null); + + verify(documentVersionService).recordVersion(any(Document.class)); + } }