From da43cadb0a4b4cba68f9bf21c2d340974120cd7b Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 5 Apr 2026 21:01:02 +0200 Subject: [PATCH] feat(comments): add block-level comment endpoints with TDD RED/GREEN for CommentService: - getCommentsForBlock(blockId): returns root comments filtered by blockId - postBlockComment(documentId, blockId, content, mentions, author): creates comment with block_id set RED/GREEN for CommentController: - GET /api/documents/{docId}/transcription-blocks/{blockId}/comments - POST /api/documents/{docId}/transcription-blocks/{blockId}/comments - POST .../comments/{commentId}/replies (reuses existing replyToComment) 4 new tests: 2 service unit tests + 2 controller integration tests All 25 CommentServiceTest + 24 CommentControllerTest green Co-Authored-By: Claude Sonnet 4.6 --- .../controller/CommentController.java | 31 ++++++++++++++++ .../repository/CommentRepository.java | 2 ++ .../service/CommentService.java | 22 ++++++++++++ .../controller/CommentControllerTest.java | 26 ++++++++++++++ .../service/CommentServiceTest.java | 36 +++++++++++++++++++ 5 files changed, 117 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/CommentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/CommentController.java index c9f9fac8..cb6b6d70 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/CommentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/CommentController.java @@ -85,6 +85,37 @@ public class CommentController { return commentService.replyToComment(documentId, commentId, dto.getContent(), dto.getMentionedUserIds(), author); } + // ─── Block (transcription) comments ──────────────────────────────────────── + + @GetMapping("/api/documents/{documentId}/transcription-blocks/{blockId}/comments") + public List getBlockComments(@PathVariable UUID blockId) { + return commentService.getCommentsForBlock(blockId); + } + + @PostMapping("/api/documents/{documentId}/transcription-blocks/{blockId}/comments") + @ResponseStatus(HttpStatus.CREATED) + @RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL}) + public DocumentComment postBlockComment( + @PathVariable UUID documentId, + @PathVariable UUID blockId, + @RequestBody CreateCommentDTO dto, + Authentication authentication) { + AppUser author = resolveUser(authentication); + return commentService.postBlockComment(documentId, blockId, dto.getContent(), dto.getMentionedUserIds(), author); + } + + @PostMapping("/api/documents/{documentId}/transcription-blocks/{blockId}/comments/{commentId}/replies") + @ResponseStatus(HttpStatus.CREATED) + @RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL}) + public DocumentComment replyToBlockComment( + @PathVariable UUID documentId, + @PathVariable UUID commentId, + @RequestBody CreateCommentDTO dto, + Authentication authentication) { + AppUser author = resolveUser(authentication); + return commentService.replyToComment(documentId, commentId, dto.getContent(), dto.getMentionedUserIds(), author); + } + // ─── Edit and delete (shared) ───────────────────────────────────────────── @PatchMapping("/api/documents/{documentId}/comments/{commentId}") diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/CommentRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/CommentRepository.java index 80269305..9327a350 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/CommentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/CommentRepository.java @@ -13,4 +13,6 @@ public interface CommentRepository extends JpaRepository List findByAnnotationIdAndParentIdIsNull(UUID annotationId); List findByParentId(UUID parentId); + + List findByBlockIdAndParentIdIsNull(UUID blockId); } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java index 4d932c84..bfd4b6df 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java @@ -34,6 +34,28 @@ public class CommentService { return withRepliesAndMentions(roots); } + public List getCommentsForBlock(UUID blockId) { + List roots = commentRepository.findByBlockIdAndParentIdIsNull(blockId); + return withRepliesAndMentions(roots); + } + + @Transactional + public DocumentComment postBlockComment(UUID documentId, UUID blockId, String content, + List mentionedUserIds, AppUser author) { + DocumentComment comment = DocumentComment.builder() + .documentId(documentId) + .blockId(blockId) + .content(content) + .authorId(author.getId()) + .authorName(resolveAuthorName(author)) + .build(); + saveMentions(comment, mentionedUserIds); + DocumentComment saved = commentRepository.save(comment); + withMentionDTOs(saved); + notificationService.notifyMentions(mentionedUserIds, saved); + return saved; + } + @Transactional public DocumentComment postComment(UUID documentId, UUID annotationId, String content, List mentionedUserIds, AppUser author) { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java index d9c2f31d..a556e676 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java @@ -279,4 +279,30 @@ class CommentControllerTest { .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) .andExpect(status().isCreated()); } + + // ─── Block comment endpoints ───────────────────────────────────────────── + + @Test + @WithMockUser + void getBlockComments_returns200() throws Exception { + UUID blockId = UUID.randomUUID(); + when(commentService.getCommentsForBlock(blockId)).thenReturn(List.of()); + + mockMvc.perform(get("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void postBlockComment_returns201() throws Exception { + UUID blockId = UUID.randomUUID(); + DocumentComment saved = DocumentComment.builder() + .id(UUID.randomUUID()).documentId(DOC_ID).blockId(blockId).content("Nice").build(); + when(commentService.postBlockComment(any(), any(), any(), any(), any())).thenReturn(saved); + + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.blockId").value(blockId.toString())); + } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java index 8373f110..94851440 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java @@ -488,4 +488,40 @@ class CommentServiceTest { .build())) .build(); } + + // ─── Block-level comments ──────────────────────────────────────────────── + + @Test + void getCommentsForBlock_returnsRootCommentsFilteredByBlockId() { + UUID blockId = UUID.randomUUID(); + DocumentComment root = DocumentComment.builder() + .id(UUID.randomUUID()).blockId(blockId).content("Nice work").authorName("Felix") + .createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build(); + when(commentRepository.findByBlockIdAndParentIdIsNull(blockId)).thenReturn(List.of(root)); + when(commentRepository.findByParentId(root.getId())).thenReturn(List.of()); + + List result = commentService.getCommentsForBlock(blockId); + + assertThat(result).hasSize(1); + assertThat(result.getFirst().getContent()).isEqualTo("Nice work"); + } + + @Test + void postBlockComment_setsBlockIdOnComment() { + UUID documentId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).username("felix").firstName("Felix").lastName("Brandt").build(); + when(commentRepository.save(any())).thenAnswer(inv -> { + DocumentComment c = inv.getArgument(0); + c.setId(UUID.randomUUID()); + return c; + }); + + DocumentComment result = commentService.postBlockComment( + documentId, blockId, "Looks like Breslau", List.of(), author); + + assertThat(result.getBlockId()).isEqualTo(blockId); + assertThat(result.getDocumentId()).isEqualTo(documentId); + assertThat(result.getContent()).isEqualTo("Looks like Breslau"); + } }