refactor(comment): drop dead document and annotation comment APIs

Only block comments are surfaced by the frontend now. The document-level
and annotation-level comment endpoints and service methods existed but
had no consumer. Remove them along with their repository queries and
test coverage so the surface area matches the actual feature set.

Shared edit, delete, and block reply endpoints stay. postBlockComment
now carries the authorName/mention/audit behaviors previously tested
through the dropped postComment method, so those behaviors remain
covered by the block-scoped test suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-21 13:27:10 +02:00
parent 13732ab96b
commit bc69e8ff1e
5 changed files with 313 additions and 556 deletions

View File

@@ -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<DocumentComment> 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<DocumentComment> 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")

View File

@@ -8,10 +8,6 @@ import java.util.UUID;
public interface CommentRepository extends JpaRepository<DocumentComment, UUID> {
List<DocumentComment> findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(UUID documentId);
List<DocumentComment> findByAnnotationIdAndParentIdIsNull(UUID annotationId);
List<DocumentComment> findByParentId(UUID parentId);
List<DocumentComment> findByBlockIdAndParentIdIsNull(UUID blockId);

View File

@@ -29,17 +29,6 @@ public class CommentService {
private final AuditService auditService;
private final TranscriptionService transcriptionService;
public List<DocumentComment> getCommentsForDocument(UUID documentId) {
List<DocumentComment> roots =
commentRepository.findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(documentId);
return withRepliesAndMentions(roots);
}
public List<DocumentComment> getCommentsForAnnotation(UUID annotationId) {
List<DocumentComment> roots = commentRepository.findByAnnotationIdAndParentIdIsNull(annotationId);
return withRepliesAndMentions(roots);
}
public List<DocumentComment> getCommentsForBlock(UUID blockId) {
List<DocumentComment> 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<UUID> 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<UUID> mentionedUserIds, AppUser author) {

View File

@@ -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());
}
}

View File

@@ -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<DocumentComment> 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<DocumentComment> 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;
});
}
}