feat(audit): add COMMENT_ADDED and MENTION_CREATED audit events
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m35s
CI / OCR Service Tests (pull_request) Successful in 39s
CI / Backend Unit Tests (pull_request) Failing after 2m54s
CI / Unit & Component Tests (push) Failing after 2m38s
CI / Backend Unit Tests (push) Failing after 2m47s
CI / OCR Service Tests (push) Successful in 35s

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 <noreply@anthropic.com>
This commit was merged in pull request #275.
This commit is contained in:
Marcel
2026-04-19 15:43:51 +02:00
parent 5a3b5ff3c7
commit 428c63a2f2
3 changed files with 157 additions and 0 deletions

View File

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

View File

@@ -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<DocumentComment> getCommentsForDocument(UUID documentId) {
List<DocumentComment> 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<UUID> 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();

View File

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