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 d34ee9a9..57911095 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/CommentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/CommentController.java @@ -24,67 +24,6 @@ public class CommentController { private final CommentService commentService; private final UserService userService; - // ─── General document comments ──────────────────────────────────────────── - - @GetMapping("/api/documents/{documentId}/comments") - public List getDocumentComments(@PathVariable UUID documentId) { - return commentService.getCommentsForDocument(documentId); - } - - @PostMapping("/api/documents/{documentId}/comments") - @ResponseStatus(HttpStatus.CREATED) - @RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL}) - public DocumentComment postDocumentComment( - @PathVariable UUID documentId, - @RequestBody CreateCommentDTO dto, - Authentication authentication) { - AppUser author = resolveUser(authentication); - return commentService.postComment(documentId, null, dto.getContent(), dto.getMentionedUserIds(), author); - } - - @PostMapping("/api/documents/{documentId}/comments/{commentId}/replies") - @ResponseStatus(HttpStatus.CREATED) - @RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL}) - public DocumentComment replyToDocumentComment( - @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); - } - - // ─── Annotation comments ────────────────────────────────────────────────── - - @GetMapping("/api/documents/{documentId}/annotations/{annotationId}/comments") - public List getAnnotationComments(@PathVariable UUID annotationId) { - return commentService.getCommentsForAnnotation(annotationId); - } - - @PostMapping("/api/documents/{documentId}/annotations/{annotationId}/comments") - @ResponseStatus(HttpStatus.CREATED) - @RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL}) - public DocumentComment postAnnotationComment( - @PathVariable UUID documentId, - @PathVariable UUID annotationId, - @RequestBody CreateCommentDTO dto, - Authentication authentication) { - AppUser author = resolveUser(authentication); - return commentService.postComment(documentId, annotationId, dto.getContent(), dto.getMentionedUserIds(), author); - } - - @PostMapping("/api/documents/{documentId}/annotations/{annotationId}/comments/{commentId}/replies") - @ResponseStatus(HttpStatus.CREATED) - @RequirePermission({Permission.ANNOTATE_ALL, Permission.WRITE_ALL}) - public DocumentComment replyToAnnotationComment( - @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); - } - // ─── Block (transcription) comments ──────────────────────────────────────── @GetMapping("/api/documents/{documentId}/transcription-blocks/{blockId}/comments") 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 9327a350..61e2da59 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/CommentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/CommentRepository.java @@ -8,10 +8,6 @@ import java.util.UUID; public interface CommentRepository extends JpaRepository { - List findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(UUID documentId); - - 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 dff407d2..554fa492 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java @@ -29,17 +29,6 @@ public class CommentService { private final AuditService auditService; private final TranscriptionService transcriptionService; - public List getCommentsForDocument(UUID documentId) { - List roots = - commentRepository.findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(documentId); - return withRepliesAndMentions(roots); - } - - public List getCommentsForAnnotation(UUID annotationId) { - List roots = commentRepository.findByAnnotationIdAndParentIdIsNull(annotationId); - return withRepliesAndMentions(roots); - } - public List getCommentsForBlock(UUID blockId) { List roots = commentRepository.findByBlockIdAndParentIdIsNull(blockId); return withRepliesAndMentions(roots); @@ -65,24 +54,6 @@ public class CommentService { return saved; } - @Transactional - public DocumentComment postComment(UUID documentId, UUID annotationId, String content, - List mentionedUserIds, AppUser author) { - DocumentComment comment = DocumentComment.builder() - .documentId(documentId) - .annotationId(annotationId) - .content(content) - .authorId(author.getId()) - .authorName(resolveAuthorName(author)) - .build(); - saveMentions(comment, mentionedUserIds); - DocumentComment saved = commentRepository.save(comment); - withMentionDTOs(saved); - notificationService.notifyMentions(mentionedUserIds, saved); - logCommentPosted(author, documentId, saved, mentionedUserIds); - return saved; - } - @Transactional public DocumentComment replyToComment(UUID documentId, UUID commentId, 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 63ad617b..58714e7f 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/CommentControllerTest.java @@ -40,246 +40,8 @@ class CommentControllerTest { private static final String COMMENT_JSON = "{\"content\":\"Test comment\"}"; private static final UUID DOC_ID = UUID.randomUUID(); - private static final UUID ANN_ID = UUID.randomUUID(); private static final UUID COMMENT_ID = UUID.randomUUID(); - // ─── GET /api/documents/{documentId}/comments ───────────────────────────── - - @Test - void getDocumentComments_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(get("/api/documents/" + DOC_ID + "/comments")) - .andExpect(status().isUnauthorized()); - } - - @Test - @WithMockUser - void getDocumentComments_returns200_whenAuthenticated() throws Exception { - when(commentService.getCommentsForDocument(any())).thenReturn(List.of()); - mockMvc.perform(get("/api/documents/" + DOC_ID + "/comments")) - .andExpect(status().isOk()); - } - - // ─── POST /api/documents/{documentId}/comments ──────────────────────────── - - @Test - void postDocumentComment_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isUnauthorized()); - } - - @Test - @WithMockUser - void postDocumentComment_returns403_whenMissingPermission() throws Exception { - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isForbidden()); - } - - @Test - @WithMockUser(authorities = "ANNOTATE_ALL") - void postDocumentComment_returns201_whenHasPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); - when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.content").value("Test comment")); - } - - @Test - @WithMockUser(authorities = "WRITE_ALL") - void postDocumentComment_returns201_whenHasWriteAllPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); - when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - - // ─── POST /api/documents/{documentId}/comments/{commentId}/replies ──────── - - @Test - void replyToComment_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID + "/replies") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isUnauthorized()); - } - - @Test - @WithMockUser(authorities = "ANNOTATE_ALL") - void replyToComment_returns201_whenHasPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(DOC_ID).parentId(COMMENT_ID) - .authorName("Anna").content("Test comment").build(); - when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID + "/replies") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - - @Test - @WithMockUser(authorities = "WRITE_ALL") - void replyToComment_returns201_whenHasWriteAllPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(DOC_ID).parentId(COMMENT_ID) - .authorName("Anna").content("Test comment").build(); - when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID + "/replies") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - - // ─── PATCH /api/documents/{documentId}/comments/{commentId} ────────────── - - @Test - void editComment_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isUnauthorized()); - } - - @Test - @WithMockUser(authorities = "ANNOTATE_ALL") - void editComment_returns200_whenHasPermission() throws Exception { - DocumentComment updated = DocumentComment.builder() - .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); - when(commentService.editComment(any(), any(), any(), any())).thenReturn(updated); - - mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isOk()); - } - - // ─── DELETE /api/documents/{documentId}/comments/{commentId} ───────────── - - @Test - void deleteComment_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID)) - .andExpect(status().isUnauthorized()); - } - - @Test - @WithMockUser - void deleteComment_returns204_whenAuthenticated() throws Exception { - mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID)) - .andExpect(status().isNoContent()); - } - - // ─── GET /api/documents/{documentId}/annotations/{annId}/comments ───────── - - @Test - void getAnnotationComments_returns401_whenUnauthenticated() throws Exception { - mockMvc.perform(get("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments")) - .andExpect(status().isUnauthorized()); - } - - @Test - @WithMockUser - void getAnnotationComments_returns200_whenAuthenticated() throws Exception { - when(commentService.getCommentsForAnnotation(any())).thenReturn(List.of()); - mockMvc.perform(get("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments")) - .andExpect(status().isOk()); - } - - @Test - @WithMockUser(authorities = "WRITE_ALL") - void editComment_returns200_whenHasWriteAllPermission() throws Exception { - DocumentComment updated = DocumentComment.builder() - .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); - when(commentService.editComment(any(), any(), any(), any())).thenReturn(updated); - - mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isOk()); - } - - // ─── POST /api/documents/{documentId}/annotations/{annId}/comments ──────── - - @Test - @WithMockUser - void postAnnotationComment_returns403_whenMissingPermission() throws Exception { - mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isForbidden()); - } - - @Test - @WithMockUser(authorities = "ANNOTATE_ALL") - void postAnnotationComment_returns201_whenHasPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID) - .authorName("Hans").content("Test comment").build(); - when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - - @Test - @WithMockUser(authorities = "WRITE_ALL") - void postAnnotationComment_returns201_whenHasWriteAllPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID) - .authorName("Hans").content("Test comment").build(); - when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - - // ─── POST /api/documents/{documentId}/annotations/{annId}/comments/{commentId}/replies ─ - - @Test - @WithMockUser(authorities = "ANNOTATE_ALL") - void replyToAnnotationComment_returns201_whenHasPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID) - .parentId(COMMENT_ID).authorName("Anna").content("Test comment").build(); - when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments/" + COMMENT_ID + "/replies") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - - @Test - @WithMockUser(authorities = "WRITE_ALL") - void replyToAnnotationComment_returns201_whenHasWriteAllPermission() throws Exception { - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID) - .parentId(COMMENT_ID).authorName("Anna").content("Test comment").build(); - when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments/" + COMMENT_ID + "/replies") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - - // ─── resolveUser — exception branch ────────────────────────────────────── - - @Test - @WithMockUser(authorities = "WRITE_ALL") - void postDocumentComment_stillSucceeds_whenUserServiceThrows() throws Exception { - // findByEmail throws → catch block in resolveUser → author null, saves anyway - when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error")); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(DOC_ID).content("Test comment").build(); - when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved); - - mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments") - .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) - .andExpect(status().isCreated()); - } - // ─── Block comment endpoints ───────────────────────────────────────────── @Test @@ -305,4 +67,138 @@ class CommentControllerTest { .andExpect(status().isCreated()) .andExpect(jsonPath("$.blockId").value(blockId.toString())); } + + @Test + void postBlockComment_returns401_whenUnauthenticated() throws Exception { + UUID blockId = UUID.randomUUID(); + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void postBlockComment_returns403_whenMissingPermission() throws Exception { + UUID blockId = UUID.randomUUID(); + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + "/comments") + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "ANNOTATE_ALL") + void postBlockComment_returns201_whenHasAnnotatePermission() 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()); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void postBlockComment_stillSucceeds_whenUserServiceThrows() throws Exception { + // findByEmail throws → catch block in resolveUser → author null, saves anyway + UUID blockId = UUID.randomUUID(); + when(userService.findByEmail(any())).thenThrow(new RuntimeException("DB error")); + DocumentComment saved = DocumentComment.builder() + .id(UUID.randomUUID()).documentId(DOC_ID).blockId(blockId).content("Test comment").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()); + } + + // ─── Block reply endpoints ─────────────────────────────────────────────── + + @Test + void replyToBlockComment_returns401_whenUnauthenticated() throws Exception { + UUID blockId = UUID.randomUUID(); + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + + "/comments/" + COMMENT_ID + "/replies") + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser(authorities = "ANNOTATE_ALL") + void replyToBlockComment_returns201_whenHasPermission() throws Exception { + UUID blockId = UUID.randomUUID(); + DocumentComment saved = DocumentComment.builder() + .id(UUID.randomUUID()).documentId(DOC_ID).blockId(blockId).parentId(COMMENT_ID) + .authorName("Anna").content("Reply").build(); + when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); + + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + + "/comments/" + COMMENT_ID + "/replies") + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void replyToBlockComment_returns201_whenHasWriteAllPermission() throws Exception { + UUID blockId = UUID.randomUUID(); + DocumentComment saved = DocumentComment.builder() + .id(UUID.randomUUID()).documentId(DOC_ID).blockId(blockId).parentId(COMMENT_ID) + .authorName("Anna").content("Reply").build(); + when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved); + + mockMvc.perform(post("/api/documents/" + DOC_ID + "/transcription-blocks/" + blockId + + "/comments/" + COMMENT_ID + "/replies") + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isCreated()); + } + + // ─── PATCH /api/documents/{documentId}/comments/{commentId} (shared edit) ── + + @Test + void editComment_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser(authorities = "ANNOTATE_ALL") + void editComment_returns200_whenHasPermission() throws Exception { + DocumentComment updated = DocumentComment.builder() + .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); + when(commentService.editComment(any(), any(), any(), any())).thenReturn(updated); + + mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void editComment_returns200_whenHasWriteAllPermission() throws Exception { + DocumentComment updated = DocumentComment.builder() + .id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build(); + when(commentService.editComment(any(), any(), any(), any())).thenReturn(updated); + + mockMvc.perform(patch("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID) + .contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON)) + .andExpect(status().isOk()); + } + + // ─── DELETE /api/documents/{documentId}/comments/{commentId} (shared) ──── + + @Test + void deleteComment_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void deleteComment_returns204_whenAuthenticated() throws Exception { + mockMvc.perform(delete("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID)) + .andExpect(status().isNoContent()); + } } 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 b72cc9fb..19eb0b8d 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java @@ -43,52 +43,6 @@ class CommentServiceTest { @Mock TranscriptionService transcriptionService; @InjectMocks CommentService commentService; - // ─── postComment ────────────────────────────────────────────────────────── - - @Test - void postComment_capturesAuthorNameAtWriteTime() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder() - .id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("Müller").build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("Hans Müller").content("Test").build(); - when(commentRepository.save(any())).thenReturn(saved); - - DocumentComment result = commentService.postComment(docId, null, "Test", List.of(), author); - - assertThat(result.getAuthorName()).isEqualTo("Hans Müller"); - } - - @Test - void postComment_fallsBackToUsername_whenNamesAreBlank() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans42@example.com").build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("hans42").content("Test").build(); - when(commentRepository.save(any())).thenReturn(saved); - - DocumentComment result = commentService.postComment(docId, null, "Test", List.of(), author); - - assertThat(result.getAuthorName()).isEqualTo("hans42"); - } - - @Test - void postComment_triggersNotifyMentions_whenMentionedUserIdsProvided() { - UUID docId = UUID.randomUUID(); - UUID mentionedId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build(); - AppUser mentioned = AppUser.builder().id(mentionedId).email("anna@example.com").firstName("Anna").lastName("S").build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("Hans M").content("Hey @Anna S").build(); - - when(userService.findAllById(List.of(mentionedId))).thenReturn(List.of(mentioned)); - when(commentRepository.save(any())).thenReturn(saved); - - commentService.postComment(docId, null, "Hey @Anna S", List.of(mentionedId), author); - - verify(notificationService).notifyMentions(eq(List.of(mentionedId)), eq(saved)); - } - // ─── replyToComment ─────────────────────────────────────────────────────── @Test @@ -224,7 +178,7 @@ class CommentServiceTest { .id(commentId).documentId(docId).authorId(authorId) .content("Original").authorName("Hans").createdAt(created).build(); when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment)); - when(commentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + stubSaveAssigningRandomId(); DocumentComment result = commentService.editComment(docId, commentId, "Updated", author); @@ -284,28 +238,6 @@ class CommentServiceTest { verify(commentRepository).delete(comment); } - // ─── getCommentsForDocument ─────────────────────────────────────────────── - - @Test - void getCommentsForDocument_returnsRootsWithRepliesAttached() { - UUID docId = UUID.randomUUID(); - UUID rootId = UUID.randomUUID(); - - DocumentComment root = DocumentComment.builder() - .id(rootId).documentId(docId).authorName("Hans").content("Root").build(); - DocumentComment reply = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).parentId(rootId).authorName("Anna").content("Reply").build(); - - when(commentRepository.findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(docId)) - .thenReturn(List.of(root)); - when(commentRepository.findByParentId(rootId)).thenReturn(List.of(reply)); - - List result = commentService.getCommentsForDocument(docId); - - assertThat(result).hasSize(1); - assertThat(result.get(0).getReplies()).containsExactly(reply); - } - // ─── replyToComment — reply with null authorId in thread ───────────────── @Test @@ -332,82 +264,6 @@ class CommentServiceTest { verify(notificationService).notifyReply(eq(saved), anySet()); } - // ─── resolveAuthorName edge cases ───────────────────────────────────────── - - @Test - void postComment_fallsBackToUsername_whenFirstNameBlankAndLastNameNull() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") - .firstName(" ").lastName(null).build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("user42").content("Hi").build(); - when(commentRepository.save(any())).thenReturn(saved); - - DocumentComment result = commentService.postComment(docId, null, "Hi", List.of(), author); - - assertThat(result.getAuthorName()).isEqualTo("user42"); - } - - @Test - void postComment_fallsBackToUsername_whenFirstNameNullAndLastNameBlank() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") - .firstName(null).lastName(" ").build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("user42").content("Hi").build(); - when(commentRepository.save(any())).thenReturn(saved); - - DocumentComment result = commentService.postComment(docId, null, "Hi", List.of(), author); - - assertThat(result.getAuthorName()).isEqualTo("user42"); - } - - @Test - void postComment_includesOnlyFirstName_whenLastNameIsNull() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") - .firstName("Hans").lastName(null).build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("Hans").content("Hi").build(); - when(commentRepository.save(any())).thenReturn(saved); - - commentService.postComment(docId, null, "Hi", List.of(), author); - - // first != null && !blank → true; last == null → entire condition false → returns stripped first - verify(commentRepository).save(any()); - } - - @Test - void postComment_includesOnlyLastName_whenFirstNameIsNull() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") - .firstName(null).lastName("Müller").build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("Müller").content("Hi").build(); - when(commentRepository.save(any())).thenReturn(saved); - - commentService.postComment(docId, null, "Hi", List.of(), author); - - // No exception — name resolution with null first name strips cleanly - verify(commentRepository).save(any()); - } - - // ─── saveMentions — null/empty guard ───────────────────────────────────── - - @Test - void postComment_doesNotCallUserService_whenMentionedUserIdsIsNull() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com") - .firstName("Hans").lastName("M").build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("Hans M").content("Hi").build(); - when(commentRepository.save(any())).thenReturn(saved); - - commentService.postComment(docId, null, "Hi", null, author); - - verify(userService, never()).findAllById(anyList()); - } - // ─── collectParticipantIds — non-null authorId in reply ────────────────── @Test @@ -461,26 +317,6 @@ class CommentServiceTest { verify(notificationService).notifyReply(eq(saved), anySet()); } - // ─── getCommentsForAnnotation ───────────────────────────────────────────── - - @Test - void getCommentsForAnnotation_returnsRootsForAnnotation() { - UUID annotationId = UUID.randomUUID(); - UUID rootId = UUID.randomUUID(); - - DocumentComment root = DocumentComment.builder() - .id(rootId).annotationId(annotationId).authorName("Hans").content("Root").build(); - - when(commentRepository.findByAnnotationIdAndParentIdIsNull(annotationId)) - .thenReturn(List.of(root)); - when(commentRepository.findByParentId(rootId)).thenReturn(List.of()); - - List result = commentService.getCommentsForAnnotation(annotationId); - - assertThat(result).hasSize(1); - assertThat(result.get(0).getAnnotationId()).isEqualTo(annotationId); - } - // ─── helpers ────────────────────────────────────────────────────────────── private AppUser buildAdmin() { @@ -497,65 +333,6 @@ class CommentServiceTest { // ─── audit: COMMENT_ADDED and MENTION_CREATED ───────────────────────────── - @Test - void postComment_logsCommentAdded() { - UUID docId = UUID.randomUUID(); - UUID savedId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build(); - DocumentComment saved = DocumentComment.builder() - .id(savedId).documentId(docId).authorName("Hans M").content("Hello").build(); - when(commentRepository.save(any())).thenReturn(saved); - - commentService.postComment(docId, null, "Hello", List.of(), author); - - verify(auditService).logAfterCommit( - eq(AuditKind.COMMENT_ADDED), - eq(author.getId()), - eq(docId), - argThat(p -> savedId.toString().equals(p.get("commentId")))); - } - - @Test - void postComment_logsMentionCreated_oncePerMentionedUser() { - UUID docId = UUID.randomUUID(); - UUID savedId = UUID.randomUUID(); - UUID mentionedId1 = UUID.randomUUID(); - UUID mentionedId2 = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build(); - AppUser mentioned1 = AppUser.builder().id(mentionedId1).email("anna@example.com").firstName("Anna").lastName("S").build(); - AppUser mentioned2 = AppUser.builder().id(mentionedId2).email("bob@example.com").firstName("Bob").lastName("J").build(); - DocumentComment saved = DocumentComment.builder() - .id(savedId).documentId(docId).authorName("Hans M").content("Hey @Anna @Bob").build(); - when(userService.findAllById(List.of(mentionedId1, mentionedId2))).thenReturn(List.of(mentioned1, mentioned2)); - when(commentRepository.save(any())).thenReturn(saved); - - commentService.postComment(docId, null, "Hey @Anna @Bob", List.of(mentionedId1, mentionedId2), author); - - verify(auditService).logAfterCommit( - eq(AuditKind.MENTION_CREATED), - eq(author.getId()), - eq(docId), - argThat(p -> mentionedId1.toString().equals(p.get("mentionedUserId")))); - verify(auditService).logAfterCommit( - eq(AuditKind.MENTION_CREATED), - eq(author.getId()), - eq(docId), - argThat(p -> mentionedId2.toString().equals(p.get("mentionedUserId")))); - } - - @Test - void postComment_doesNotLogMentionCreated_whenNoMentions() { - UUID docId = UUID.randomUUID(); - AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build(); - DocumentComment saved = DocumentComment.builder() - .id(UUID.randomUUID()).documentId(docId).authorName("Hans M").content("Hello").build(); - when(commentRepository.save(any())).thenReturn(saved); - - commentService.postComment(docId, null, "Hello", List.of(), author); - - verify(auditService, never()).logAfterCommit(eq(AuditKind.MENTION_CREATED), any(), any(), any()); - } - @Test void replyToComment_logsCommentAdded() { UUID docId = UUID.randomUUID(); @@ -699,4 +476,182 @@ class CommentServiceTest { .isInstanceOf(DomainException.class) .hasMessageContaining("Transcription block not found"); } + + // ─── postBlockComment — authorName resolution ──────────────────────────── + + @Test + void postBlockComment_capturesAuthorNameAtWriteTime() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder() + .id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("Müller").build(); + stubBlock(docId, blockId); + stubSaveAssigningRandomId(); + + DocumentComment result = commentService.postBlockComment(docId, blockId, "Test", List.of(), author); + + assertThat(result.getAuthorName()).isEqualTo("Hans Müller"); + } + + @Test + void postBlockComment_fallsBackToEmail_whenNamesAreBlank() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans42@example.com").build(); + stubBlock(docId, blockId); + stubSaveAssigningRandomId(); + + DocumentComment result = commentService.postBlockComment(docId, blockId, "Test", List.of(), author); + + assertThat(result.getAuthorName()).isEqualTo("hans42@example.com"); + } + + @Test + void postBlockComment_fallsBackToEmail_whenFirstNameBlankAndLastNameNull() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") + .firstName(" ").lastName(null).build(); + stubBlock(docId, blockId); + stubSaveAssigningRandomId(); + + DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author); + + assertThat(result.getAuthorName()).isEqualTo("user42@example.com"); + } + + @Test + void postBlockComment_fallsBackToEmail_whenFirstNameNullAndLastNameBlank() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") + .firstName(null).lastName(" ").build(); + stubBlock(docId, blockId); + stubSaveAssigningRandomId(); + + DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author); + + assertThat(result.getAuthorName()).isEqualTo("user42@example.com"); + } + + @Test + void postBlockComment_usesFirstNameAlone_whenLastNameIsNull() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") + .firstName("Hans").lastName(null).build(); + stubBlock(docId, blockId); + stubSaveAssigningRandomId(); + + DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author); + + assertThat(result.getAuthorName()).isEqualTo("Hans"); + } + + @Test + void postBlockComment_usesLastNameAlone_whenFirstNameIsNull() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com") + .firstName(null).lastName("Müller").build(); + stubBlock(docId, blockId); + stubSaveAssigningRandomId(); + + DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author); + + assertThat(result.getAuthorName()).isEqualTo("Müller"); + } + + // ─── postBlockComment — mentions ───────────────────────────────────────── + + @Test + void postBlockComment_triggersNotifyMentions_whenMentionedUserIdsProvided() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + UUID mentionedId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build(); + AppUser mentioned = AppUser.builder().id(mentionedId).email("anna@example.com").firstName("Anna").lastName("S").build(); + DocumentComment saved = DocumentComment.builder() + .id(UUID.randomUUID()).documentId(docId).blockId(blockId).authorName("Hans M").content("Hey @Anna S").build(); + stubBlock(docId, blockId); + when(userService.findAllById(List.of(mentionedId))).thenReturn(List.of(mentioned)); + when(commentRepository.save(any())).thenReturn(saved); + + commentService.postBlockComment(docId, blockId, "Hey @Anna S", List.of(mentionedId), author); + + verify(notificationService).notifyMentions(eq(List.of(mentionedId)), eq(saved)); + } + + @Test + void postBlockComment_doesNotCallUserService_whenMentionedUserIdsIsNull() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com") + .firstName("Hans").lastName("M").build(); + stubBlock(docId, blockId); + stubSaveAssigningRandomId(); + + commentService.postBlockComment(docId, blockId, "Hi", null, author); + + verify(userService, never()).findAllById(anyList()); + } + + @Test + void postBlockComment_logsMentionCreated_oncePerMentionedUser() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + UUID savedId = UUID.randomUUID(); + UUID mentionedId1 = UUID.randomUUID(); + UUID mentionedId2 = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build(); + AppUser mentioned1 = AppUser.builder().id(mentionedId1).email("anna@example.com").firstName("Anna").lastName("S").build(); + AppUser mentioned2 = AppUser.builder().id(mentionedId2).email("bob@example.com").firstName("Bob").lastName("J").build(); + DocumentComment saved = DocumentComment.builder() + .id(savedId).documentId(docId).blockId(blockId).authorName("Hans M").content("Hey @Anna @Bob").build(); + stubBlock(docId, blockId); + when(userService.findAllById(List.of(mentionedId1, mentionedId2))).thenReturn(List.of(mentioned1, mentioned2)); + when(commentRepository.save(any())).thenReturn(saved); + + commentService.postBlockComment(docId, blockId, "Hey @Anna @Bob", List.of(mentionedId1, mentionedId2), author); + + verify(auditService).logAfterCommit( + eq(AuditKind.MENTION_CREATED), + eq(author.getId()), + eq(docId), + argThat(p -> mentionedId1.toString().equals(p.get("mentionedUserId")))); + verify(auditService).logAfterCommit( + eq(AuditKind.MENTION_CREATED), + eq(author.getId()), + eq(docId), + argThat(p -> mentionedId2.toString().equals(p.get("mentionedUserId")))); + } + + @Test + void postBlockComment_doesNotLogMentionCreated_whenNoMentions() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build(); + DocumentComment saved = DocumentComment.builder() + .id(UUID.randomUUID()).documentId(docId).blockId(blockId).authorName("Hans M").content("Hello").build(); + stubBlock(docId, blockId); + when(commentRepository.save(any())).thenReturn(saved); + + commentService.postBlockComment(docId, blockId, "Hello", List.of(), author); + + verify(auditService, never()).logAfterCommit(eq(AuditKind.MENTION_CREATED), any(), any(), any()); + } + + private void stubBlock(UUID docId, UUID blockId) { + when(transcriptionService.getBlock(docId, blockId)) + .thenReturn(TranscriptionBlock.builder() + .id(blockId).documentId(docId).annotationId(UUID.randomUUID()).sortOrder(0).build()); + } + + private void stubSaveAssigningRandomId() { + when(commentRepository.save(any())).thenAnswer(inv -> { + DocumentComment c = inv.getArgument(0); + if (c.getId() == null) c.setId(UUID.randomUUID()); + return c; + }); + } }