feat: notifications, @mentions, and comment deep-links (#71 #72 #73) #127

Merged
marcel merged 19 commits from feat/71-72-73-notifications-mentions-deeplinks into main 2026-03-28 16:06:59 +01:00
2 changed files with 25 additions and 1 deletions
Showing only changes of commit bc62f3b0af - Show all commits

View File

@@ -17,6 +17,7 @@ import java.util.UUID;
public class CommentService {
private final CommentRepository commentRepository;
private final NotificationService notificationService;
Review

Layering violation: CommentService directly injects AppUserRepository.

From CLAUDE.md (strictly enforced):

Services never reach into another domain's repository. Call the other domain's service instead.

AppUserRepository belongs to the user domain. CommentService should inject UserService and call a method like userService.findAllById(mentionedUserIds). If UserService doesn't expose that method yet, add it there.

Same violation exists in NotificationService, which injects both AppUserRepository and CommentRepository. NotificationService is a new service so there's more latitude, but it should still go through UserService for user lookups and CommentService for comment lookups rather than reaching into their repositories directly.

**Layering violation: `CommentService` directly injects `AppUserRepository`.** From CLAUDE.md (strictly enforced): > Services never reach into another domain's repository. Call the other domain's service instead. `AppUserRepository` belongs to the user domain. `CommentService` should inject `UserService` and call a method like `userService.findAllById(mentionedUserIds)`. If `UserService` doesn't expose that method yet, add it there. Same violation exists in `NotificationService`, which injects both `AppUserRepository` and `CommentRepository`. `NotificationService` is a new service so there's more latitude, but it should still go through `UserService` for user lookups and `CommentService` for comment lookups rather than reaching into their repositories directly.
public List<DocumentComment> getCommentsForDocument(UUID documentId) {
List<DocumentComment> roots =
@@ -60,7 +61,9 @@ public class CommentService {
.authorId(author.getId())
.authorName(resolveAuthorName(author))
.build();
return commentRepository.save(reply);
DocumentComment saved = commentRepository.save(reply);
notificationService.notifyReply(saved, root);
return saved;
}
@Transactional

View File

@@ -20,6 +20,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -30,6 +31,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
class CommentServiceTest {
@Mock CommentRepository commentRepository;
@Mock NotificationService notificationService;
@InjectMocks CommentService commentService;
// ─── postComment ──────────────────────────────────────────────────────────
@@ -119,6 +121,25 @@ class CommentServiceTest {
assertThat(result.getParentId()).isEqualTo(rootId);
}
@Test
void replyToComment_triggersNotification_afterSave() {
UUID docId = UUID.randomUUID();
UUID rootId = UUID.randomUUID();
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build();
DocumentComment root = DocumentComment.builder()
.id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
DocumentComment saved = DocumentComment.builder()
.id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply").authorName("anna").build();
when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
when(commentRepository.save(any())).thenReturn(saved);
commentService.replyToComment(docId, rootId, "Reply", author);
verify(notificationService).notifyReply(eq(saved), eq(root));
}
// ─── editComment ──────────────────────────────────────────────────────────
@Test