refactor(backend): split ThumbnailService.generate into stages with distinct logs

Addresses @felixbrandt — fix(backend): "the two try blocks in generate()
overlap — a save failure logs 'generation failed' even though the
thumbnail is already in S3 as an orphan".

generate() now orchestrates four stages, each in its own try+log:
  readSourceImage / encodeThumbnail / uploadToStorage / persistThumbnailMetadata

persistThumbnailMetadata emits the distinct "orphaned in storage as <key>"
log line so an operator can see database-side failures after the upload
completed. The deterministic key ensures the next run overwrites cleanly,
so the orphan is self-healing.

Also extracts THUMBNAIL_KEY_PREFIX/SUFFIX constants with a comment
explaining the deterministic-overwrite contract.

Adds test: generate_returnsFailed_whenPersistThrows_butUploadSucceeded.

Refs #307

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-22 23:01:50 +02:00
parent f137aa79a2
commit f0f9753c42
2 changed files with 74 additions and 8 deletions

View File

@@ -167,6 +167,24 @@ class ThumbnailServiceTest {
verify(documentRepository, never()).save(any());
}
@Test
void generate_returnsFailed_whenPersistThrows_butUploadSucceeded() throws IOException {
// Covers the "orphan thumbnail" edge case: S3 upload succeeded but the
// entity update blew up. We must still return FAILED so the backfill
// tally is honest, without losing the fact that we already put bytes in S3.
Document doc = makeDoc("application/pdf", "documents/letter.pdf");
when(fileService.downloadFileStream(anyString()))
.thenReturn(new ByteArrayInputStream(createSamplePdf()));
when(documentRepository.save(any()))
.thenThrow(new RuntimeException("constraint violation"));
ThumbnailService.Outcome outcome = thumbnailService.generate(doc);
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.FAILED);
verify(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));
verify(documentRepository).save(any());
}
// ─── helpers ──────────────────────────────────────────────────────────────
private Document makeDoc(String contentType, String filePath) {