diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceTest.java new file mode 100644 index 00000000..efc6de6a --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceTest.java @@ -0,0 +1,247 @@ +package org.raddatz.familienarchiv.service; + +import org.junit.jupiter.api.Test; +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.dto.CreateAnnotationDTO; +import org.raddatz.familienarchiv.dto.CreateTranscriptionBlockDTO; +import org.raddatz.familienarchiv.dto.ReorderTranscriptionBlocksDTO; +import org.raddatz.familienarchiv.dto.UpdateTranscriptionBlockDTO; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.model.Document; +import org.raddatz.familienarchiv.model.DocumentAnnotation; +import org.raddatz.familienarchiv.model.TranscriptionBlock; +import org.raddatz.familienarchiv.model.TranscriptionBlockVersion; +import org.raddatz.familienarchiv.repository.AnnotationRepository; +import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; +import org.raddatz.familienarchiv.repository.TranscriptionBlockVersionRepository; + +import java.util.List; +import java.util.Optional; +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.verify; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@ExtendWith(MockitoExtension.class) +class TranscriptionServiceTest { + + @Mock TranscriptionBlockRepository blockRepository; + @Mock TranscriptionBlockVersionRepository versionRepository; + @Mock AnnotationRepository annotationRepository; + @Mock AnnotationService annotationService; + @Mock DocumentService documentService; + @InjectMocks TranscriptionService transcriptionService; + + // ─── getBlock ──────────────────────────────────────────────────────────────── + + @Test + void getBlock_throwsNotFound_whenBlockDoesNotExist() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> transcriptionService.getBlock(docId, blockId)) + .isInstanceOf(DomainException.class) + .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND)); + } + + @Test + void getBlock_returnsBlock_whenExists() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + TranscriptionBlock block = TranscriptionBlock.builder() + .id(blockId).documentId(docId).text("hello").build(); + when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block)); + + TranscriptionBlock result = transcriptionService.getBlock(docId, blockId); + + assertThat(result).isEqualTo(block); + } + + // ─── createBlock ───────────────────────────────────────────────────────────── + + @Test + void createBlock_createsAnnotationAndBlockAndVersion() { + UUID docId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + UUID annotId = UUID.randomUUID(); + + Document doc = Document.builder().id(docId).fileHash("hash123").build(); + when(documentService.getDocumentById(docId)).thenReturn(doc); + + DocumentAnnotation annotation = DocumentAnnotation.builder().id(annotId).build(); + when(annotationService.createAnnotation(eq(docId), any(CreateAnnotationDTO.class), eq(userId), eq("hash123"))) + .thenReturn(annotation); + + when(blockRepository.countByDocumentId(docId)).thenReturn(0); + when(blockRepository.save(any())).thenAnswer(inv -> { + TranscriptionBlock b = inv.getArgument(0); + b.setId(UUID.randomUUID()); + return b; + }); + + CreateTranscriptionBlockDTO dto = new CreateTranscriptionBlockDTO(1, 0.1, 0.2, 0.3, 0.4, "hello", null); + + TranscriptionBlock result = transcriptionService.createBlock(docId, dto, userId); + + assertThat(result.getAnnotationId()).isEqualTo(annotId); + assertThat(result.getText()).isEqualTo("hello"); + assertThat(result.getSortOrder()).isZero(); + assertThat(result.getCreatedBy()).isEqualTo(userId); + verify(versionRepository).save(any(TranscriptionBlockVersion.class)); + } + + // ─── updateBlock ───────────────────────────────────────────────────────────── + + @Test + void updateBlock_updatesTextAndSavesVersion() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + + TranscriptionBlock block = TranscriptionBlock.builder() + .id(blockId).documentId(docId).text("old").build(); + when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block)); + when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + UpdateTranscriptionBlockDTO dto = new UpdateTranscriptionBlockDTO("new text", null); + + TranscriptionBlock result = transcriptionService.updateBlock(docId, blockId, dto, userId); + + assertThat(result.getText()).isEqualTo("new text"); + assertThat(result.getUpdatedBy()).isEqualTo(userId); + verify(versionRepository).save(any(TranscriptionBlockVersion.class)); + } + + @Test + void updateBlock_updatesLabel_whenProvided() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + + TranscriptionBlock block = TranscriptionBlock.builder() + .id(blockId).documentId(docId).text("text").label("old label").build(); + when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block)); + when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + UpdateTranscriptionBlockDTO dto = new UpdateTranscriptionBlockDTO("text", "Anrede"); + + TranscriptionBlock result = transcriptionService.updateBlock(docId, blockId, dto, UUID.randomUUID()); + + assertThat(result.getLabel()).isEqualTo("Anrede"); + } + + // ─── deleteBlock ───────────────────────────────────────────────────────────── + + @Test + void deleteBlock_deletesBlockAndAnnotation() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + UUID annotId = UUID.randomUUID(); + + TranscriptionBlock block = TranscriptionBlock.builder() + .id(blockId).documentId(docId).annotationId(annotId).build(); + when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block)); + + transcriptionService.deleteBlock(docId, blockId); + + verify(blockRepository).delete(block); + verify(blockRepository).flush(); + verify(annotationRepository).deleteById(annotId); + } + + @Test + void deleteBlock_throwsNotFound_whenBlockMissing() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> transcriptionService.deleteBlock(docId, blockId)) + .isInstanceOf(DomainException.class) + .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND)); + } + + // ─── reorderBlocks ─────────────────────────────────────────────────────────── + + @Test + void reorderBlocks_updatesSortOrder() { + UUID docId = UUID.randomUUID(); + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + + TranscriptionBlock block1 = TranscriptionBlock.builder() + .id(id1).documentId(docId).sortOrder(0).build(); + TranscriptionBlock block2 = TranscriptionBlock.builder() + .id(id2).documentId(docId).sortOrder(1).build(); + + when(blockRepository.findByIdAndDocumentId(id2, docId)).thenReturn(Optional.of(block2)); + when(blockRepository.findByIdAndDocumentId(id1, docId)).thenReturn(Optional.of(block1)); + when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId)).thenReturn(List.of(block2, block1)); + + ReorderTranscriptionBlocksDTO dto = new ReorderTranscriptionBlocksDTO(List.of(id2, id1)); + + transcriptionService.reorderBlocks(docId, dto); + + assertThat(block2.getSortOrder()).isZero(); + assertThat(block1.getSortOrder()).isEqualTo(1); + } + + // ─── getBlockHistory ───────────────────────────────────────────────────────── + + @Test + void getBlockHistory_returnsVersionsForBlock() { + UUID docId = UUID.randomUUID(); + UUID blockId = UUID.randomUUID(); + + TranscriptionBlock block = TranscriptionBlock.builder() + .id(blockId).documentId(docId).build(); + when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block)); + + TranscriptionBlockVersion v = TranscriptionBlockVersion.builder() + .id(UUID.randomUUID()).blockId(blockId).text("ver1").build(); + when(versionRepository.findByBlockIdOrderByChangedAtDesc(blockId)).thenReturn(List.of(v)); + + List result = transcriptionService.getBlockHistory(docId, blockId); + + assertThat(result).containsExactly(v); + } + + // ─── sanitizeText ──────────────────────────────────────────────────────────── + + @Test + void sanitizeText_returnsEmptyString_forNull() { + assertThat(transcriptionService.sanitizeText(null)).isEmpty(); + } + + @Test + void sanitizeText_truncatesAtMaxLength() { + String longText = "a".repeat(15_000); + String result = transcriptionService.sanitizeText(longText); + assertThat(result).hasSize(10_000); + } + + @Test + void sanitizeText_preservesPlainText() { + assertThat(transcriptionService.sanitizeText("Liebe Mutter,")).isEqualTo("Liebe Mutter,"); + } + + // ─── listBlocks ────────────────────────────────────────────────────────────── + + @Test + void listBlocks_returnsBlocksOrderedBySortOrder() { + UUID docId = UUID.randomUUID(); + TranscriptionBlock b = TranscriptionBlock.builder() + .id(UUID.randomUUID()).documentId(docId).sortOrder(0).build(); + when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId)).thenReturn(List.of(b)); + + assertThat(transcriptionService.listBlocks(docId)).containsExactly(b); + } +}