refactor(thumbnail): route document access through DocumentService
The Thumbnail trio (ThumbnailService, ThumbnailBackfillService, ThumbnailAsyncRunner) all injected DocumentRepository directly. They now go through three new DocumentService delegations: - findById(UUID): Optional<Document> — no-throw variant for the runner's log-and-skip behaviour on missing documents. - findForThumbnailBackfill() — wraps the existing findByFilePathIsNotNullAndThumbnailKeyIsNull query. - updateThumbnailMetadata(Document) — wraps save() for the post-thumbnail entity update. DocumentService also gains @Lazy on its existing ThumbnailAsyncRunner field to break the new DocumentService ↔ ThumbnailAsyncRunner cycle. lombok.config adds @Lazy to copyableAnnotations so the field annotation reaches the generated constructor parameter. Refs #417 (C6.2 violations #2, #3, #4). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1
backend/lombok.config
Normal file
1
backend/lombok.config
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
|
||||||
@@ -25,6 +25,7 @@ import org.raddatz.familienarchiv.model.TrainingLabel;
|
|||||||
import org.raddatz.familienarchiv.model.Person;
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
import org.raddatz.familienarchiv.model.Tag;
|
import org.raddatz.familienarchiv.model.Tag;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -69,6 +70,10 @@ public class DocumentService {
|
|||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
|
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
|
||||||
private final AuditLogQueryService auditLogQueryService;
|
private final AuditLogQueryService auditLogQueryService;
|
||||||
|
// @Lazy breaks the DocumentService ↔ ThumbnailAsyncRunner cycle: the runner
|
||||||
|
// now reaches Document data through DocumentService (per the layering rule),
|
||||||
|
// and Spring needs a proxy here to defer the back-edge until both beans exist.
|
||||||
|
@Lazy
|
||||||
private final ThumbnailAsyncRunner thumbnailAsyncRunner;
|
private final ThumbnailAsyncRunner thumbnailAsyncRunner;
|
||||||
|
|
||||||
public record StoreResult(Document document, boolean isNew) {}
|
public record StoreResult(Document document, boolean isNew) {}
|
||||||
@@ -77,6 +82,18 @@ public class DocumentService {
|
|||||||
return documentRepository.count();
|
return documentRepository.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<Document> findById(UUID id) {
|
||||||
|
return documentRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Document> findForThumbnailBackfill() {
|
||||||
|
return documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Document updateThumbnailMetadata(Document doc) {
|
||||||
|
return documentRepository.save(doc);
|
||||||
|
}
|
||||||
|
|
||||||
public Map<UUID, String> findTitlesByIds(Collection<UUID> ids) {
|
public Map<UUID, String> findTitlesByIds(Collection<UUID> ids) {
|
||||||
if (ids.isEmpty()) return Map.of();
|
if (ids.isEmpty()) return Map.of();
|
||||||
Map<UUID, String> titles = new HashMap<>();
|
Map<UUID, String> titles = new HashMap<>();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.raddatz.familienarchiv.service;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.support.TransactionSynchronization;
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
@@ -29,7 +28,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ThumbnailAsyncRunner {
|
public class ThumbnailAsyncRunner {
|
||||||
|
|
||||||
private final DocumentRepository documentRepository;
|
private final DocumentService documentService;
|
||||||
private final ThumbnailService thumbnailService;
|
private final ThumbnailService thumbnailService;
|
||||||
|
|
||||||
/** Per-document timeout for the whole generate() call — defense against corrupt PDFs. */
|
/** Per-document timeout for the whole generate() call — defense against corrupt PDFs. */
|
||||||
@@ -60,7 +59,7 @@ public class ThumbnailAsyncRunner {
|
|||||||
*/
|
*/
|
||||||
@Async("thumbnailExecutor")
|
@Async("thumbnailExecutor")
|
||||||
public void generateAsync(UUID documentId) {
|
public void generateAsync(UUID documentId) {
|
||||||
Optional<Document> docOpt = documentRepository.findById(documentId);
|
Optional<Document> docOpt = documentService.findById(documentId);
|
||||||
if (docOpt.isEmpty()) {
|
if (docOpt.isEmpty()) {
|
||||||
log.warn("Thumbnail generation skipped: document not found id={}", documentId);
|
log.warn("Thumbnail generation skipped: document not found id={}", documentId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ public class ThumbnailBackfillService {
|
|||||||
LocalDateTime startedAt
|
LocalDateTime startedAt
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private final DocumentRepository documentRepository;
|
private final DocumentService documentService;
|
||||||
private final ThumbnailService thumbnailService;
|
private final ThumbnailService thumbnailService;
|
||||||
|
|
||||||
private volatile BackfillStatus currentStatus = new BackfillStatus(
|
private volatile BackfillStatus currentStatus = new BackfillStatus(
|
||||||
@@ -57,7 +56,7 @@ public class ThumbnailBackfillService {
|
|||||||
LocalDateTime startedAt = LocalDateTime.now();
|
LocalDateTime startedAt = LocalDateTime.now();
|
||||||
List<Document> docs;
|
List<Document> docs;
|
||||||
try {
|
try {
|
||||||
docs = documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull();
|
docs = documentService.findForThumbnailBackfill();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
currentStatus = new BackfillStatus(State.FAILED,
|
currentStatus = new BackfillStatus(State.FAILED,
|
||||||
"Backfill fehlgeschlagen: " + e.getMessage(),
|
"Backfill fehlgeschlagen: " + e.getMessage(),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import org.apache.pdfbox.rendering.ImageType;
|
|||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
@@ -62,16 +61,16 @@ public class ThumbnailService {
|
|||||||
|
|
||||||
private final FileService fileService;
|
private final FileService fileService;
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
private final DocumentRepository documentRepository;
|
private final DocumentService documentService;
|
||||||
|
|
||||||
@Value("${app.s3.bucket}")
|
@Value("${app.s3.bucket}")
|
||||||
private String bucketName;
|
private String bucketName;
|
||||||
|
|
||||||
public ThumbnailService(FileService fileService, S3Client s3Client,
|
public ThumbnailService(FileService fileService, S3Client s3Client,
|
||||||
DocumentRepository documentRepository) {
|
DocumentService documentService) {
|
||||||
this.fileService = fileService;
|
this.fileService = fileService;
|
||||||
this.s3Client = s3Client;
|
this.s3Client = s3Client;
|
||||||
this.documentRepository = documentRepository;
|
this.documentService = documentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Outcome generate(Document doc) {
|
public Outcome generate(Document doc) {
|
||||||
@@ -167,7 +166,7 @@ public class ThumbnailService {
|
|||||||
doc.setThumbnailGeneratedAt(LocalDateTime.now());
|
doc.setThumbnailGeneratedAt(LocalDateTime.now());
|
||||||
doc.setThumbnailAspect(result.aspect());
|
doc.setThumbnailAspect(result.aspect());
|
||||||
doc.setPageCount(result.pageCount());
|
doc.setPageCount(result.pageCount());
|
||||||
documentRepository.save(doc);
|
documentService.updateThumbnailMetadata(doc);
|
||||||
return Outcome.SUCCESS;
|
return Outcome.SUCCESS;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Thumbnail is already in S3 but the entity update failed. Because the S3
|
// Thumbnail is already in S3 but the entity update failed. Because the S3
|
||||||
|
|||||||
@@ -2264,4 +2264,46 @@ class DocumentServiceTest {
|
|||||||
assertThat(doc.getArchiveFolder()).isEqualTo("KeepFolder");
|
assertThat(doc.getArchiveFolder()).isEqualTo("KeepFolder");
|
||||||
assertThat(doc.getDocumentLocation()).isEqualTo("KeepLocation");
|
assertThat(doc.getDocumentLocation()).isEqualTo("KeepLocation");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── findById (no-throw variant) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findById_returnsEmpty_whenDocumentDoesNotExist() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
when(documentRepository.findById(id)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThat(documentService.findById(id)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findById_returnsDocument_whenPresent() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
Document doc = Document.builder().id(id).title("T").build();
|
||||||
|
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
|
||||||
|
|
||||||
|
assertThat(documentService.findById(id)).contains(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── findForThumbnailBackfill ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findForThumbnailBackfill_returnsRepositoryResult() {
|
||||||
|
Document a = Document.builder().id(UUID.randomUUID()).title("A").build();
|
||||||
|
Document b = Document.builder().id(UUID.randomUUID()).title("B").build();
|
||||||
|
when(documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull())
|
||||||
|
.thenReturn(List.of(a, b));
|
||||||
|
|
||||||
|
assertThat(documentService.findForThumbnailBackfill()).containsExactly(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── updateThumbnailMetadata ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateThumbnailMetadata_savesDocument() {
|
||||||
|
Document doc = Document.builder().id(UUID.randomUUID()).title("T").build();
|
||||||
|
when(documentRepository.save(doc)).thenReturn(doc);
|
||||||
|
|
||||||
|
assertThat(documentService.updateThumbnailMetadata(doc)).isEqualTo(doc);
|
||||||
|
verify(documentRepository).save(doc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import org.junit.jupiter.api.BeforeEach;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.transaction.support.TransactionSynchronization;
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
@@ -18,22 +17,22 @@ import static org.mockito.Mockito.*;
|
|||||||
|
|
||||||
class ThumbnailAsyncRunnerTest {
|
class ThumbnailAsyncRunnerTest {
|
||||||
|
|
||||||
private DocumentRepository documentRepository;
|
private DocumentService documentService;
|
||||||
private ThumbnailService thumbnailService;
|
private ThumbnailService thumbnailService;
|
||||||
private ThumbnailAsyncRunner runner;
|
private ThumbnailAsyncRunner runner;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
documentRepository = mock(DocumentRepository.class);
|
documentService = mock(DocumentService.class);
|
||||||
thumbnailService = mock(ThumbnailService.class);
|
thumbnailService = mock(ThumbnailService.class);
|
||||||
runner = new ThumbnailAsyncRunner(documentRepository, thumbnailService);
|
runner = new ThumbnailAsyncRunner(documentService, thumbnailService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void dispatchAfterCommit_whenNoTransaction_dispatchesImmediately() {
|
void dispatchAfterCommit_whenNoTransaction_dispatchesImmediately() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
||||||
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
|
when(documentService.findById(id)).thenReturn(Optional.of(doc));
|
||||||
|
|
||||||
runner.dispatchAfterCommit(id);
|
runner.dispatchAfterCommit(id);
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ class ThumbnailAsyncRunnerTest {
|
|||||||
void dispatchAfterCommit_whenTransactionActive_registersAfterCommitSynchronization() {
|
void dispatchAfterCommit_whenTransactionActive_registersAfterCommitSynchronization() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
||||||
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
|
when(documentService.findById(id)).thenReturn(Optional.of(doc));
|
||||||
|
|
||||||
TransactionSynchronizationManager.initSynchronization();
|
TransactionSynchronizationManager.initSynchronization();
|
||||||
try {
|
try {
|
||||||
@@ -69,7 +68,7 @@ class ThumbnailAsyncRunnerTest {
|
|||||||
void dispatchAfterCommit_whenRollback_doesNotDispatch() {
|
void dispatchAfterCommit_whenRollback_doesNotDispatch() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
||||||
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
|
when(documentService.findById(id)).thenReturn(Optional.of(doc));
|
||||||
|
|
||||||
TransactionSynchronizationManager.initSynchronization();
|
TransactionSynchronizationManager.initSynchronization();
|
||||||
try {
|
try {
|
||||||
@@ -88,7 +87,7 @@ class ThumbnailAsyncRunnerTest {
|
|||||||
@Test
|
@Test
|
||||||
void generateAsync_skipsWhenDocumentMissing() {
|
void generateAsync_skipsWhenDocumentMissing() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
when(documentRepository.findById(id)).thenReturn(Optional.empty());
|
when(documentService.findById(id)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
runner.generateAsync(id);
|
runner.generateAsync(id);
|
||||||
|
|
||||||
@@ -99,7 +98,7 @@ class ThumbnailAsyncRunnerTest {
|
|||||||
void generateAsync_timesOutWhenGenerateExceedsLimit() throws Exception {
|
void generateAsync_timesOutWhenGenerateExceedsLimit() throws Exception {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
|
||||||
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
|
when(documentService.findById(id)).thenReturn(Optional.of(doc));
|
||||||
// generate sleeps longer than the timeout — simulates a hung PDFBox render
|
// generate sleeps longer than the timeout — simulates a hung PDFBox render
|
||||||
when(thumbnailService.generate(doc)).thenAnswer(inv -> {
|
when(thumbnailService.generate(doc)).thenAnswer(inv -> {
|
||||||
Thread.sleep(5_000);
|
Thread.sleep(5_000);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -19,15 +18,15 @@ import static org.mockito.Mockito.*;
|
|||||||
|
|
||||||
class ThumbnailBackfillServiceTest {
|
class ThumbnailBackfillServiceTest {
|
||||||
|
|
||||||
private DocumentRepository documentRepository;
|
private DocumentService documentService;
|
||||||
private ThumbnailService thumbnailService;
|
private ThumbnailService thumbnailService;
|
||||||
private ThumbnailBackfillService backfillService;
|
private ThumbnailBackfillService backfillService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
documentRepository = mock(DocumentRepository.class);
|
documentService = mock(DocumentService.class);
|
||||||
thumbnailService = mock(ThumbnailService.class);
|
thumbnailService = mock(ThumbnailService.class);
|
||||||
backfillService = new ThumbnailBackfillService(documentRepository, thumbnailService);
|
backfillService = new ThumbnailBackfillService(documentService, thumbnailService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -45,7 +44,7 @@ class ThumbnailBackfillServiceTest {
|
|||||||
Document a = doc();
|
Document a = doc();
|
||||||
Document b = doc();
|
Document b = doc();
|
||||||
Document c = doc();
|
Document c = doc();
|
||||||
when(documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull())
|
when(documentService.findForThumbnailBackfill())
|
||||||
.thenReturn(List.of(a, b, c));
|
.thenReturn(List.of(a, b, c));
|
||||||
when(thumbnailService.generate(any())).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
when(thumbnailService.generate(any())).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ class ThumbnailBackfillServiceTest {
|
|||||||
void runBackfillAsync_countsSkippedSeparately() {
|
void runBackfillAsync_countsSkippedSeparately() {
|
||||||
Document a = doc();
|
Document a = doc();
|
||||||
Document b = doc();
|
Document b = doc();
|
||||||
when(documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull())
|
when(documentService.findForThumbnailBackfill())
|
||||||
.thenReturn(List.of(a, b));
|
.thenReturn(List.of(a, b));
|
||||||
when(thumbnailService.generate(a)).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
when(thumbnailService.generate(a)).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
||||||
when(thumbnailService.generate(b)).thenReturn(ThumbnailService.Outcome.SKIPPED);
|
when(thumbnailService.generate(b)).thenReturn(ThumbnailService.Outcome.SKIPPED);
|
||||||
@@ -83,7 +82,7 @@ class ThumbnailBackfillServiceTest {
|
|||||||
Document a = doc();
|
Document a = doc();
|
||||||
Document b = doc();
|
Document b = doc();
|
||||||
Document c = doc();
|
Document c = doc();
|
||||||
when(documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull())
|
when(documentService.findForThumbnailBackfill())
|
||||||
.thenReturn(List.of(a, b, c));
|
.thenReturn(List.of(a, b, c));
|
||||||
when(thumbnailService.generate(a)).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
when(thumbnailService.generate(a)).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
||||||
when(thumbnailService.generate(b)).thenReturn(ThumbnailService.Outcome.FAILED);
|
when(thumbnailService.generate(b)).thenReturn(ThumbnailService.Outcome.FAILED);
|
||||||
@@ -102,7 +101,7 @@ class ThumbnailBackfillServiceTest {
|
|||||||
void runBackfillAsync_continuesWhenServiceThrowsUnexpectedException() {
|
void runBackfillAsync_continuesWhenServiceThrowsUnexpectedException() {
|
||||||
Document a = doc();
|
Document a = doc();
|
||||||
Document b = doc();
|
Document b = doc();
|
||||||
when(documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull())
|
when(documentService.findForThumbnailBackfill())
|
||||||
.thenReturn(List.of(a, b));
|
.thenReturn(List.of(a, b));
|
||||||
when(thumbnailService.generate(a)).thenThrow(new RuntimeException("boom"));
|
when(thumbnailService.generate(a)).thenThrow(new RuntimeException("boom"));
|
||||||
when(thumbnailService.generate(b)).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
when(thumbnailService.generate(b)).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
||||||
@@ -130,7 +129,7 @@ class ThumbnailBackfillServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void runBackfillAsync_setsStartedAtAndMessage() {
|
void runBackfillAsync_setsStartedAtAndMessage() {
|
||||||
when(documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull())
|
when(documentService.findForThumbnailBackfill())
|
||||||
.thenReturn(List.of(doc()));
|
.thenReturn(List.of(doc()));
|
||||||
when(thumbnailService.generate(any())).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
when(thumbnailService.generate(any())).thenReturn(ThumbnailService.Outcome.SUCCESS);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import org.mockito.ArgumentCaptor;
|
|||||||
import org.raddatz.familienarchiv.model.Document;
|
import org.raddatz.familienarchiv.model.Document;
|
||||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||||
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
||||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
@@ -39,17 +38,17 @@ class ThumbnailServiceTest {
|
|||||||
|
|
||||||
private FileService fileService;
|
private FileService fileService;
|
||||||
private S3Client s3Client;
|
private S3Client s3Client;
|
||||||
private DocumentRepository documentRepository;
|
private DocumentService documentService;
|
||||||
private ThumbnailService thumbnailService;
|
private ThumbnailService thumbnailService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
fileService = mock(FileService.class);
|
fileService = mock(FileService.class);
|
||||||
s3Client = mock(S3Client.class);
|
s3Client = mock(S3Client.class);
|
||||||
documentRepository = mock(DocumentRepository.class);
|
documentService = mock(DocumentService.class);
|
||||||
thumbnailService = new ThumbnailService(fileService, s3Client, documentRepository);
|
thumbnailService = new ThumbnailService(fileService, s3Client, documentService);
|
||||||
ReflectionTestUtils.setField(thumbnailService, "bucketName", "test-bucket");
|
ReflectionTestUtils.setField(thumbnailService, "bucketName", "test-bucket");
|
||||||
when(documentRepository.save(any(Document.class))).thenAnswer(i -> i.getArgument(0));
|
when(documentService.updateThumbnailMetadata(any(Document.class))).thenAnswer(i -> i.getArgument(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -103,7 +102,7 @@ class ThumbnailServiceTest {
|
|||||||
|
|
||||||
assertThat(doc.getThumbnailKey()).isEqualTo("thumbnails/" + doc.getId() + ".jpg");
|
assertThat(doc.getThumbnailKey()).isEqualTo("thumbnails/" + doc.getId() + ".jpg");
|
||||||
assertThat(doc.getThumbnailGeneratedAt()).isNotNull();
|
assertThat(doc.getThumbnailGeneratedAt()).isNotNull();
|
||||||
verify(documentRepository).save(doc);
|
verify(documentService).updateThumbnailMetadata(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -152,7 +151,7 @@ class ThumbnailServiceTest {
|
|||||||
|
|
||||||
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
||||||
assertThat(doc.getThumbnailKey()).isNull();
|
assertThat(doc.getThumbnailKey()).isNull();
|
||||||
verify(documentRepository, never()).save(any());
|
verify(documentService, never()).updateThumbnailMetadata(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -165,7 +164,7 @@ class ThumbnailServiceTest {
|
|||||||
|
|
||||||
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
||||||
verifyNoInteractions(s3Client);
|
verifyNoInteractions(s3Client);
|
||||||
verify(documentRepository, never()).save(any());
|
verify(documentService, never()).updateThumbnailMetadata(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -260,7 +259,7 @@ class ThumbnailServiceTest {
|
|||||||
|
|
||||||
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
||||||
verifyNoInteractions(s3Client);
|
verifyNoInteractions(s3Client);
|
||||||
verify(documentRepository, never()).save(any());
|
verify(documentService, never()).updateThumbnailMetadata(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -275,7 +274,7 @@ class ThumbnailServiceTest {
|
|||||||
|
|
||||||
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
||||||
verifyNoInteractions(s3Client);
|
verifyNoInteractions(s3Client);
|
||||||
verify(documentRepository, never()).save(any());
|
verify(documentService, never()).updateThumbnailMetadata(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -286,14 +285,14 @@ class ThumbnailServiceTest {
|
|||||||
Document doc = makeDoc("application/pdf", "documents/letter.pdf");
|
Document doc = makeDoc("application/pdf", "documents/letter.pdf");
|
||||||
when(fileService.downloadFileStream(anyString()))
|
when(fileService.downloadFileStream(anyString()))
|
||||||
.thenReturn(new ByteArrayInputStream(createSamplePdf()));
|
.thenReturn(new ByteArrayInputStream(createSamplePdf()));
|
||||||
when(documentRepository.save(any()))
|
when(documentService.updateThumbnailMetadata(any()))
|
||||||
.thenThrow(new RuntimeException("constraint violation"));
|
.thenThrow(new RuntimeException("constraint violation"));
|
||||||
|
|
||||||
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
|
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
|
||||||
|
|
||||||
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
|
||||||
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
|
||||||
verify(documentRepository).save(any());
|
verify(documentService).updateThumbnailMetadata(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── helpers ──────────────────────────────────────────────────────────────
|
// ─── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user