From 428c63a2f2d0aa513a1d57d2ae501b20d163e988 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 19 Apr 2026 15:43:51 +0200 Subject: [PATCH] feat(audit): add COMMENT_ADDED and MENTION_CREATED audit events Instruments CommentService.postComment(), postBlockComment(), and replyToComment() to fire COMMENT_ADDED after each successful save and MENTION_CREATED once per mentioned user. The shared logCommentPosted() helper avoids duplicating the two-call pattern across all three post methods. Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/audit/AuditKind.java | 6 + .../service/CommentService.java | 18 +++ .../service/CommentServiceTest.java | 133 ++++++++++++++++++ 3 files changed, 157 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java index cac1ef48..d44513b5 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/audit/AuditKind.java @@ -19,4 +19,10 @@ public enum AuditKind { /** Payload: {@code {"pageNumber": 3}} */ ANNOTATION_CREATED, + + /** Payload: {@code {"commentId": "uuid"}} */ + COMMENT_ADDED, + + /** Payload: {@code {"commentId": "uuid", "mentionedUserId": "uuid"}} */ + MENTION_CREATED, } 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 f80955e2..e6157509 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/CommentService.java @@ -1,6 +1,8 @@ package org.raddatz.familienarchiv.service; import lombok.RequiredArgsConstructor; +import org.raddatz.familienarchiv.audit.AuditKind; +import org.raddatz.familienarchiv.audit.AuditService; import org.raddatz.familienarchiv.dto.MentionDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.ErrorCode; @@ -12,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -22,6 +25,7 @@ public class CommentService { private final CommentRepository commentRepository; private final UserService userService; private final NotificationService notificationService; + private final AuditService auditService; public List getCommentsForDocument(UUID documentId) { List roots = @@ -53,6 +57,7 @@ public class CommentService { DocumentComment saved = commentRepository.save(comment); withMentionDTOs(saved); notificationService.notifyMentions(mentionedUserIds, saved); + logCommentPosted(author, documentId, saved, mentionedUserIds); return saved; } @@ -70,6 +75,7 @@ public class CommentService { DocumentComment saved = commentRepository.save(comment); withMentionDTOs(saved); notificationService.notifyMentions(mentionedUserIds, saved); + logCommentPosted(author, documentId, saved, mentionedUserIds); return saved; } @@ -101,6 +107,7 @@ public class CommentService { participantIds.remove(author.getId()); notificationService.notifyReply(saved, participantIds); notificationService.notifyMentions(mentionedUserIds, saved); + logCommentPosted(author, documentId, saved, mentionedUserIds); return saved; } @@ -171,6 +178,17 @@ public class CommentService { ErrorCode.COMMENT_NOT_FOUND, "Comment not found: " + commentId)); } + private void logCommentPosted(AppUser author, UUID documentId, DocumentComment saved, List mentionedUserIds) { + UUID actorId = author != null ? author.getId() : null; + String commentId = saved.getId().toString(); + auditService.logAfterCommit(AuditKind.COMMENT_ADDED, actorId, documentId, Map.of("commentId", commentId)); + if (mentionedUserIds != null) { + mentionedUserIds.forEach(mentionedUserId -> + auditService.logAfterCommit(AuditKind.MENTION_CREATED, actorId, documentId, + Map.of("commentId", commentId, "mentionedUserId", mentionedUserId.toString()))); + } + } + private String resolveAuthorName(AppUser author) { String first = author.getFirstName(); String last = author.getLastName(); 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 6a846f03..a6596d80 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/CommentServiceTest.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.raddatz.familienarchiv.audit.AuditKind; +import org.raddatz.familienarchiv.audit.AuditService; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.DocumentComment; @@ -22,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -35,6 +38,7 @@ class CommentServiceTest { @Mock CommentRepository commentRepository; @Mock UserService userService; @Mock NotificationService notificationService; + @Mock AuditService auditService; @InjectMocks CommentService commentService; // ─── postComment ────────────────────────────────────────────────────────── @@ -489,6 +493,135 @@ class CommentServiceTest { .build(); } + // ─── 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(); + UUID rootId = UUID.randomUUID(); + UUID savedId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").firstName("Anna").lastName("S").build(); + DocumentComment root = DocumentComment.builder() + .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build(); + DocumentComment saved = DocumentComment.builder() + .id(savedId).documentId(docId).parentId(rootId).content("Reply").authorName("Anna S").build(); + when(commentRepository.findById(rootId)).thenReturn(Optional.of(root)); + when(commentRepository.findByParentId(rootId)).thenReturn(List.of()); + when(commentRepository.save(any())).thenReturn(saved); + + commentService.replyToComment(docId, rootId, "Reply", 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 replyToComment_logsMentionCreated_whenMentioned() { + UUID docId = UUID.randomUUID(); + UUID rootId = UUID.randomUUID(); + UUID savedId = UUID.randomUUID(); + UUID mentionedId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").firstName("Anna").lastName("S").build(); + AppUser mentioned = AppUser.builder().id(mentionedId).email("bob@example.com").firstName("Bob").lastName("J").build(); + DocumentComment root = DocumentComment.builder() + .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build(); + DocumentComment saved = DocumentComment.builder() + .id(savedId).documentId(docId).parentId(rootId).content("Hey @Bob").authorName("Anna S").build(); + when(userService.findAllById(List.of(mentionedId))).thenReturn(List.of(mentioned)); + when(commentRepository.findById(rootId)).thenReturn(Optional.of(root)); + when(commentRepository.findByParentId(rootId)).thenReturn(List.of()); + when(commentRepository.save(any())).thenReturn(saved); + + commentService.replyToComment(docId, rootId, "Hey @Bob", List.of(mentionedId), author); + + verify(auditService).logAfterCommit( + eq(AuditKind.MENTION_CREATED), + eq(author.getId()), + eq(docId), + argThat(p -> mentionedId.toString().equals(p.get("mentionedUserId")))); + } + + @Test + void postBlockComment_logsCommentAdded() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + UUID savedId = UUID.randomUUID(); + AppUser author = AppUser.builder().id(UUID.randomUUID()).email("felix@example.com").firstName("Felix").lastName("B").build(); + DocumentComment saved = DocumentComment.builder() + .id(savedId).documentId(docId).blockId(blockId).authorName("Felix B").content("Nice").build(); + when(commentRepository.save(any())).thenReturn(saved); + + commentService.postBlockComment(docId, blockId, "Nice", List.of(), author); + + verify(auditService).logAfterCommit( + eq(AuditKind.COMMENT_ADDED), + eq(author.getId()), + eq(docId), + argThat(p -> savedId.toString().equals(p.get("commentId")))); + } + // ─── Block-level comments ──────────────────────────────────────────────── @Test