fix(backend): resolve cross-domain repo + controller→repo violations (#417) #420

Merged
marcel merged 9 commits from feat/issue-417-resolve-layering-violations into main 2026-05-05 10:50:04 +02:00
4 changed files with 82 additions and 55 deletions
Showing only changes of commit 0ca95d5ad7 - Show all commits

View File

@@ -94,6 +94,14 @@ public class DocumentService {
return documentRepository.save(doc);
}
public Optional<Document> findByOriginalFilename(String originalFilename) {
return documentRepository.findByOriginalFilename(originalFilename);
}
public Document save(Document doc) {
return documentRepository.save(doc);
}
public List<org.raddatz.familienarchiv.repository.TranscriptionQueueProjection> findSegmentationQueue(int limit) {
return documentRepository.findSegmentationQueue(limit);
}

View File

@@ -10,7 +10,6 @@ import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@@ -55,7 +54,7 @@ public class MassImportService {
return currentStatus;
}
private final DocumentRepository documentRepository;
private final DocumentService documentService;
private final PersonService personService;
private final TagService tagService;
private final S3Client s3Client;
@@ -257,7 +256,7 @@ public class MassImportService {
@Transactional
protected void importSingleDocument(List<String> cells, Optional<File> file, String originalFilename, String index) {
Optional<Document> existing = documentRepository.findByOriginalFilename(originalFilename);
Optional<Document> existing = documentService.findByOriginalFilename(originalFilename);
if (existing.isPresent() && existing.get().getStatus() != DocumentStatus.PLACEHOLDER) {
log.info("Dokument {} existiert bereits, überspringe.", originalFilename);
return;
@@ -333,7 +332,7 @@ public class MassImportService {
if (tag != null) doc.getTags().add(tag);
doc.setMetadataComplete(metadataComplete);
Document saved = documentRepository.save(doc);
Document saved = documentService.save(doc);
if (file.isPresent()) {
thumbnailAsyncRunner.dispatchAfterCommit(saved.getId());
}

View File

@@ -2306,4 +2306,25 @@ class DocumentServiceTest {
assertThat(documentService.updateThumbnailMetadata(doc)).isEqualTo(doc);
verify(documentRepository).save(doc);
}
// ─── findByOriginalFilename ────────────────────────────────────────────────
@Test
void findByOriginalFilename_returnsRepositoryResult() {
Document doc = Document.builder().id(UUID.randomUUID()).title("T").build();
when(documentRepository.findByOriginalFilename("scan.pdf")).thenReturn(Optional.of(doc));
assertThat(documentService.findByOriginalFilename("scan.pdf")).contains(doc);
}
// ─── save ──────────────────────────────────────────────────────────────────
@Test
void save_delegatesToRepository() {
Document doc = Document.builder().id(UUID.randomUUID()).title("T").build();
when(documentRepository.save(doc)).thenReturn(doc);
assertThat(documentService.save(doc)).isEqualTo(doc);
verify(documentRepository).save(doc);
}
}

View File

@@ -11,7 +11,6 @@ import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.springframework.test.util.ReflectionTestUtils;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
@@ -35,7 +34,7 @@ import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MassImportServiceTest {
@Mock DocumentRepository documentRepository;
@Mock DocumentService documentService;
@Mock PersonService personService;
@Mock TagService tagService;
@Mock S3Client s3Client;
@@ -45,7 +44,7 @@ class MassImportServiceTest {
@BeforeEach
void setUp() {
service = new MassImportService(documentRepository, personService, tagService, s3Client, thumbnailAsyncRunner);
service = new MassImportService(documentService, personService, tagService, s3Client, thumbnailAsyncRunner);
ReflectionTestUtils.setField(service, "bucketName", "test-bucket");
ReflectionTestUtils.setField(service, "colIndex", 0);
ReflectionTestUtils.setField(service, "colBox", 1);
@@ -96,23 +95,23 @@ class MassImportServiceTest {
.originalFilename("doc001.pdf")
.status(DocumentStatus.UPLOADED)
.build();
when(documentRepository.findByOriginalFilename("doc001.pdf")).thenReturn(Optional.of(existing));
when(documentService.findByOriginalFilename("doc001.pdf")).thenReturn(Optional.of(existing));
service.importSingleDocument(minimalCells("doc001.pdf"), Optional.empty(), "doc001.pdf", "doc001");
verify(documentRepository, never()).save(any());
verify(documentService, never()).save(any());
}
// ─── importSingleDocument — create new document (metadata only) ───────────
@Test
void importSingleDocument_createsNewDocument_whenNotExists() {
when(documentRepository.findByOriginalFilename("doc002.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("doc002.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
service.importSingleDocument(minimalCells("doc002.pdf"), Optional.empty(), "doc002.pdf", "doc002");
verify(documentRepository).save(argThat(d ->
verify(documentService).save(argThat(d ->
d.getOriginalFilename().equals("doc002.pdf")
&& d.getStatus() == DocumentStatus.PLACEHOLDER));
}
@@ -126,12 +125,12 @@ class MassImportServiceTest {
.originalFilename("existing.pdf")
.status(DocumentStatus.PLACEHOLDER)
.build();
when(documentRepository.findByOriginalFilename("existing.pdf")).thenReturn(Optional.of(placeholder));
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("existing.pdf")).thenReturn(Optional.of(placeholder));
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
service.importSingleDocument(minimalCells("existing.pdf"), Optional.empty(), "existing.pdf", "existing");
verify(documentRepository).save(same(placeholder));
verify(documentService).save(same(placeholder));
}
// ─── importSingleDocument — with file (S3 upload) ─────────────────────────
@@ -141,14 +140,14 @@ class MassImportServiceTest {
Path tempFile = tempDir.resolve("doc003.pdf");
Files.write(tempFile, "PDF content".getBytes());
when(documentRepository.findByOriginalFilename("doc003.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("doc003.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
service.importSingleDocument(
minimalCells("doc003.pdf"), Optional.of(tempFile.toFile()), "doc003.pdf", "doc003");
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
verify(documentRepository).save(argThat(d -> d.getStatus() == DocumentStatus.UPLOADED));
verify(documentService).save(argThat(d -> d.getStatus() == DocumentStatus.UPLOADED));
}
@Test
@@ -156,42 +155,42 @@ class MassImportServiceTest {
Path tempFile = tempDir.resolve("fail.pdf");
Files.write(tempFile, "data".getBytes());
when(documentRepository.findByOriginalFilename("fail.pdf")).thenReturn(Optional.empty());
when(documentService.findByOriginalFilename("fail.pdf")).thenReturn(Optional.empty());
doThrow(new RuntimeException("S3 error"))
.when(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
service.importSingleDocument(
minimalCells("fail.pdf"), Optional.of(tempFile.toFile()), "fail.pdf", "fail");
verify(documentRepository, never()).save(any());
verify(documentService, never()).save(any());
}
// ─── importSingleDocument — sender handling ───────────────────────────────
@Test
void importSingleDocument_setsNullSender_whenSenderCellIsBlank() {
when(documentRepository.findByOriginalFilename("nosender.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("nosender.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
List<String> cells = buildCells("nosender.pdf", "", "", "");
service.importSingleDocument(cells, Optional.empty(), "nosender.pdf", "nosender");
verify(documentRepository).save(argThat(d -> d.getSender() == null));
verify(documentService).save(argThat(d -> d.getSender() == null));
verify(personService, never()).findOrCreateByAlias(any());
}
@Test
void importSingleDocument_createsSender_whenSenderCellIsNonBlank() {
Person sender = Person.builder().id(UUID.randomUUID()).firstName("Walter").lastName("Müller").build();
when(documentRepository.findByOriginalFilename("withsender.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("withsender.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(personService.findOrCreateByAlias("Walter Müller")).thenReturn(sender);
List<String> cells = buildCells("withsender.pdf", "Walter Müller", "", "");
service.importSingleDocument(cells, Optional.empty(), "withsender.pdf", "withsender");
verify(personService).findOrCreateByAlias("Walter Müller");
verify(documentRepository).save(argThat(d -> d.getSender() == sender));
verify(documentService).save(argThat(d -> d.getSender() == sender));
}
// ─── importSingleDocument — tag handling ─────────────────────────────────
@@ -199,8 +198,8 @@ class MassImportServiceTest {
@Test
void importSingleDocument_createsTag_whenTagCellIsNonBlank() {
Tag tag = Tag.builder().id(UUID.randomUUID()).name("Familie").build();
when(documentRepository.findByOriginalFilename("tagged.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("tagged.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(tagService.findOrCreate("Familie")).thenReturn(tag);
List<String> cells = buildCells("tagged.pdf", "", "", "Familie");
@@ -211,8 +210,8 @@ class MassImportServiceTest {
@Test
void importSingleDocument_doesNotCreateTag_whenTagCellIsBlank() {
when(documentRepository.findByOriginalFilename("notag.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("notag.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
List<String> cells = buildCells("notag.pdf", "", "", "");
service.importSingleDocument(cells, Optional.empty(), "notag.pdf", "notag");
@@ -225,38 +224,38 @@ class MassImportServiceTest {
@Test
void importSingleDocument_metadataComplete_whenSenderPresent() {
Person sender = Person.builder().id(UUID.randomUUID()).firstName("A").lastName("B").build();
when(documentRepository.findByOriginalFilename("meta.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("meta.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(personService.findOrCreateByAlias("A B")).thenReturn(sender);
List<String> cells = buildCells("meta.pdf", "A B", "", "");
service.importSingleDocument(cells, Optional.empty(), "meta.pdf", "meta");
verify(documentRepository).save(argThat(Document::isMetadataComplete));
verify(documentService).save(argThat(Document::isMetadataComplete));
}
@Test
void importSingleDocument_metadataIncomplete_whenNoKeyFieldsPresent() {
when(documentRepository.findByOriginalFilename("nometa.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("nometa.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
List<String> cells = buildCells("nometa.pdf", "", "", "");
service.importSingleDocument(cells, Optional.empty(), "nometa.pdf", "nometa");
verify(documentRepository).save(argThat(d -> !d.isMetadataComplete()));
verify(documentService).save(argThat(d -> !d.isMetadataComplete()));
}
// ─── importSingleDocument — blank fields set to null ─────────────────────
@Test
void importSingleDocument_setsBlankFieldsToNull() {
when(documentRepository.findByOriginalFilename("blank.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("blank.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
List<String> cells = buildCells("blank.pdf", "", "", "");
service.importSingleDocument(cells, Optional.empty(), "blank.pdf", "blank");
verify(documentRepository).save(argThat(d ->
verify(documentService).save(argThat(d ->
d.getLocation() == null &&
d.getSummary() == null &&
d.getTranscription() == null &&
@@ -281,13 +280,13 @@ class MassImportServiceTest {
);
Integer result = ReflectionTestUtils.invokeMethod(service, "processRows", rows);
assertThat(result).isEqualTo(0);
verify(documentRepository, never()).findByOriginalFilename(any());
verify(documentService, never()).findByOriginalFilename(any());
}
@Test
void processRows_addsExtension_whenIndexHasNoDot() {
when(documentRepository.findByOriginalFilename("doc001.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("doc001.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
List<List<String>> rows = List.of(
List.of("header"),
@@ -296,13 +295,13 @@ class MassImportServiceTest {
Integer result = ReflectionTestUtils.invokeMethod(service, "processRows", rows);
assertThat(result).isEqualTo(1);
verify(documentRepository).findByOriginalFilename("doc001.pdf");
verify(documentService).findByOriginalFilename("doc001.pdf");
}
@Test
void processRows_usesFilenameAsIs_whenIndexHasDot() {
when(documentRepository.findByOriginalFilename("doc002.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("doc002.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
List<List<String>> rows = List.of(
List.of("header"),
@@ -311,15 +310,15 @@ class MassImportServiceTest {
Integer result = ReflectionTestUtils.invokeMethod(service, "processRows", rows);
assertThat(result).isEqualTo(1);
verify(documentRepository).findByOriginalFilename("doc002.pdf");
verify(documentService).findByOriginalFilename("doc002.pdf");
}
// ─── importSingleDocument — non-blank optional fields ────────────────────
@Test
void importSingleDocument_setsNonNullOptionalFields_whenPresent() {
when(documentRepository.findByOriginalFilename("rich.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("rich.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
// box=1, folder=2, location=9, summary=11, transcription=13
List<String> cells = List.of(
@@ -341,7 +340,7 @@ class MassImportServiceTest {
service.importSingleDocument(cells, Optional.empty(), "rich.pdf", "rich");
verify(documentRepository).save(argThat(d ->
verify(documentService).save(argThat(d ->
"Box A".equals(d.getArchiveBox()) &&
"Folder B".equals(d.getArchiveFolder()) &&
"Hamburg".equals(d.getLocation()) &&
@@ -352,27 +351,27 @@ class MassImportServiceTest {
@Test
void importSingleDocument_setsMetadataComplete_whenReceiversArePresent() {
Person receiver = Person.builder().id(UUID.randomUUID()).firstName("Walter").lastName("Müller").build();
when(documentRepository.findByOriginalFilename("rcv.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("rcv.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(personService.findOrCreateByAlias("Walter Müller")).thenReturn(receiver);
List<String> cells = List.of(
"rcv.pdf", "", "", "", "", "Walter Müller", "", "", "", "", "", "", "", "");
service.importSingleDocument(cells, Optional.empty(), "rcv.pdf", "rcv");
verify(documentRepository).save(argThat(Document::isMetadataComplete));
verify(documentService).save(argThat(Document::isMetadataComplete));
}
@Test
void importSingleDocument_setsMetadataComplete_whenDateIsPresent() {
when(documentRepository.findByOriginalFilename("dated.pdf")).thenReturn(Optional.empty());
when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(documentService.findByOriginalFilename("dated.pdf")).thenReturn(Optional.empty());
when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0));
List<String> cells = List.of(
"dated.pdf", "", "", "", "", "", "", "2024-03-15", "", "", "", "", "", "");
service.importSingleDocument(cells, Optional.empty(), "dated.pdf", "dated");
verify(documentRepository).save(argThat(Document::isMetadataComplete));
verify(documentService).save(argThat(Document::isMetadataComplete));
}
// ─── buildTitle — null location ───────────────────────────────────────────