feat(audit): instrument TranscriptionService for TEXT_SAVED and BLOCK_REVIEWED

- reviewBlock: add userId param; log BLOCK_REVIEWED only on false→true
- updateBlock: log TEXT_SAVED only when text actually changes; include
  pageNumber in payload (resolved from annotation)
- Both events deferred via afterCommit() when inside a transaction
- Update TranscriptionBlockController to pass user to reviewBlock()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-19 13:24:22 +02:00
parent 793b863096
commit 9887968236
5 changed files with 131 additions and 10 deletions

View File

@@ -362,10 +362,11 @@ class TranscriptionBlockControllerTest {
@Test
@WithMockUser(authorities = "WRITE_ALL")
void reviewBlock_returns200_withToggledBlock() throws Exception {
when(userService.findByEmail(any())).thenReturn(mockUser());
TranscriptionBlock reviewed = TranscriptionBlock.builder()
.id(BLOCK_ID).documentId(DOC_ID).annotationId(UUID.randomUUID())
.text("text").sortOrder(0).reviewed(true).build();
when(transcriptionService.reviewBlock(DOC_ID, BLOCK_ID)).thenReturn(reviewed);
when(transcriptionService.reviewBlock(eq(DOC_ID), eq(BLOCK_ID), any())).thenReturn(reviewed);
mockMvc.perform(put("/api/documents/{documentId}/transcription-blocks/{blockId}/review",
DOC_ID, BLOCK_ID))

View File

@@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.model.BlockSource;
import org.raddatz.familienarchiv.model.TranscriptionBlock;
import org.raddatz.familienarchiv.repository.AnnotationRepository;
@@ -23,6 +24,7 @@ class TranscriptionServiceGuidedTest {
AnnotationService annotationService;
DocumentService documentService;
SenderModelService senderModelService;
AuditService auditService;
TranscriptionService service;
UUID docId = UUID.randomUUID();
@@ -37,9 +39,10 @@ class TranscriptionServiceGuidedTest {
annotationService = mock(AnnotationService.class);
documentService = mock(DocumentService.class);
senderModelService = mock(SenderModelService.class);
auditService = mock(AuditService.class);
service = new TranscriptionService(blockRepository, versionRepository,
annotationRepository, annotationService, documentService, senderModelService);
annotationRepository, annotationService, documentService, senderModelService, auditService);
when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

View File

@@ -2,9 +2,12 @@ package org.raddatz.familienarchiv.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
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.dto.CreateAnnotationDTO;
import org.raddatz.familienarchiv.dto.CreateTranscriptionBlockDTO;
import org.raddatz.familienarchiv.dto.ReorderTranscriptionBlocksDTO;
@@ -21,6 +24,8 @@ import org.raddatz.familienarchiv.repository.AnnotationRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockVersionRepository;
import java.util.Map;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -42,6 +47,7 @@ class TranscriptionServiceTest {
@Mock AnnotationService annotationService;
@Mock DocumentService documentService;
@Mock SenderModelService senderModelService;
@Mock AuditService auditService;
@InjectMocks TranscriptionService transcriptionService;
// ─── getBlock ────────────────────────────────────────────────────────────────
@@ -386,13 +392,14 @@ class TranscriptionServiceTest {
void reviewBlock_setsReviewedTrue() {
UUID docId = UUID.randomUUID();
UUID blockId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
TranscriptionBlock block = TranscriptionBlock.builder()
.id(blockId).documentId(docId).annotationId(UUID.randomUUID())
.text("corrected text").sortOrder(0).reviewed(false).build();
when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
TranscriptionBlock result = transcriptionService.reviewBlock(docId, blockId);
TranscriptionBlock result = transcriptionService.reviewBlock(docId, blockId, userId);
assertThat(result.isReviewed()).isTrue();
verify(blockRepository).save(block);
@@ -402,13 +409,14 @@ class TranscriptionServiceTest {
void reviewBlock_togglesReviewedFalse_whenAlreadyReviewed() {
UUID docId = UUID.randomUUID();
UUID blockId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
TranscriptionBlock block = TranscriptionBlock.builder()
.id(blockId).documentId(docId).annotationId(UUID.randomUUID())
.text("corrected text").sortOrder(0).reviewed(true).build();
when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
TranscriptionBlock result = transcriptionService.reviewBlock(docId, blockId);
TranscriptionBlock result = transcriptionService.reviewBlock(docId, blockId, userId);
assertThat(result.isReviewed()).isFalse();
}
@@ -419,7 +427,82 @@ class TranscriptionServiceTest {
UUID blockId = UUID.randomUUID();
when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.empty());
assertThatThrownBy(() -> transcriptionService.reviewBlock(docId, blockId))
assertThatThrownBy(() -> transcriptionService.reviewBlock(docId, blockId, UUID.randomUUID()))
.isInstanceOf(DomainException.class);
}
@Test
void reviewBlock_logsBlockReviewed_whenFlippingFalseToTrue() {
UUID docId = UUID.randomUUID();
UUID blockId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
TranscriptionBlock block = TranscriptionBlock.builder()
.id(blockId).documentId(docId).reviewed(false).build();
when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
transcriptionService.reviewBlock(docId, blockId, userId);
verify(auditService).log(AuditKind.BLOCK_REVIEWED, userId, docId, null);
}
@Test
void reviewBlock_doesNotLogEvent_whenFlippingTrueToFalse() {
UUID docId = UUID.randomUUID();
UUID blockId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
TranscriptionBlock block = TranscriptionBlock.builder()
.id(blockId).documentId(docId).reviewed(true).build();
when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
transcriptionService.reviewBlock(docId, blockId, userId);
verify(auditService, never()).log(any(), any(), any(), any());
}
@Test
void updateBlock_logsTextSaved_whenTextChanges() {
UUID docId = UUID.randomUUID();
UUID blockId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
UUID annotId = UUID.randomUUID();
TranscriptionBlock block = TranscriptionBlock.builder()
.id(blockId).documentId(docId).annotationId(annotId).text("old text").build();
DocumentAnnotation annotation = DocumentAnnotation.builder()
.id(annotId).pageNumber(3).build();
when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.getDocumentById(any())).thenReturn(
Document.builder().scriptType(ScriptType.TYPEWRITER).build());
when(annotationRepository.findById(annotId)).thenReturn(Optional.of(annotation));
transcriptionService.updateBlock(docId, blockId, new UpdateTranscriptionBlockDTO("new text", null), userId);
@SuppressWarnings("unchecked")
ArgumentCaptor<Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(Map.class);
verify(auditService).log(
org.mockito.ArgumentMatchers.eq(AuditKind.TEXT_SAVED),
org.mockito.ArgumentMatchers.eq(userId),
org.mockito.ArgumentMatchers.eq(docId),
payloadCaptor.capture());
assertThat(payloadCaptor.getValue()).containsEntry("pageNumber", 3);
}
@Test
void updateBlock_doesNotLogEvent_whenTextUnchanged() {
UUID docId = UUID.randomUUID();
UUID blockId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
TranscriptionBlock block = TranscriptionBlock.builder()
.id(blockId).documentId(docId).annotationId(UUID.randomUUID()).text("same text").build();
when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.getDocumentById(any())).thenReturn(
Document.builder().scriptType(ScriptType.TYPEWRITER).build());
transcriptionService.updateBlock(docId, blockId, new UpdateTranscriptionBlockDTO("same text", null), userId);
verify(auditService, never()).log(any(), any(), any(), any());
}
}