test(backend): add ThumbnailServiceIntegrationTest against real MinIO
Spins up a MinIO container (Testcontainers GenericContainer) alongside the existing PostgresContainerConfig, uploads a sample PDF, runs the real ThumbnailService, and reads the resulting JPEG back from the object store. Catches S3 signing / path-style access issues a mocked S3Client wouldn't — justifies the CI cost (~45s) per walkthrough T9b. Refs #307 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
package org.raddatz.familienarchiv.service;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Full round-trip integration test against real MinIO and real Postgres. Catches S3
|
||||
* signing / presigning issues that a mocked S3Client would miss — the rest of the
|
||||
* test pyramid mocks at the FileService boundary.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@Import(PostgresContainerConfig.class)
|
||||
class ThumbnailServiceIntegrationTest {
|
||||
|
||||
private static final String BUCKET = "archive-documents";
|
||||
private static final String ACCESS_KEY = "minioadmin";
|
||||
private static final String SECRET_KEY = "minioadmin";
|
||||
|
||||
static GenericContainer<?> minio = new GenericContainer<>("minio/minio:RELEASE.2024-06-13T22-53-53Z")
|
||||
.withEnv("MINIO_ROOT_USER", ACCESS_KEY)
|
||||
.withEnv("MINIO_ROOT_PASSWORD", SECRET_KEY)
|
||||
.withCommand("server /data")
|
||||
.withExposedPorts(9000);
|
||||
|
||||
static {
|
||||
minio.start();
|
||||
}
|
||||
|
||||
@DynamicPropertySource
|
||||
static void s3Properties(DynamicPropertyRegistry registry) {
|
||||
registry.add("app.s3.endpoint", () -> "http://" + minio.getHost() + ":" + minio.getMappedPort(9000));
|
||||
registry.add("app.s3.access-key", () -> ACCESS_KEY);
|
||||
registry.add("app.s3.secret-key", () -> SECRET_KEY);
|
||||
registry.add("app.s3.bucket", () -> BUCKET);
|
||||
registry.add("app.s3.region", () -> "eu-central-1");
|
||||
}
|
||||
|
||||
@Autowired S3Client s3Client;
|
||||
@Autowired ThumbnailService thumbnailService;
|
||||
@Autowired DocumentRepository documentRepository;
|
||||
|
||||
@Test
|
||||
void generate_writesDecodableJpegToMinio_readbackMatches() throws IOException {
|
||||
// Ensure bucket exists (the real app has a bootstrap container for this; in tests we do it here).
|
||||
// Re-creating is a no-op; wrap in try/catch because the SDK throws on "already owned".
|
||||
try (S3Client bootstrap = buildClient()) {
|
||||
try {
|
||||
bootstrap.createBucket(CreateBucketRequest.builder().bucket(BUCKET).build());
|
||||
} catch (Exception ignored) {
|
||||
// already exists
|
||||
}
|
||||
}
|
||||
|
||||
// Persist first so Hibernate assigns the UUID — avoids StaleObjectState on a pre-set id
|
||||
Document persisted = documentRepository.save(Document.builder()
|
||||
.title("IT Doc")
|
||||
.originalFilename("test.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.contentType("application/pdf")
|
||||
.build());
|
||||
UUID docId = persisted.getId();
|
||||
String pdfKey = "documents/" + docId + "_test.pdf";
|
||||
|
||||
s3Client.putObject(PutObjectRequest.builder()
|
||||
.bucket(BUCKET)
|
||||
.key(pdfKey)
|
||||
.contentType("application/pdf")
|
||||
.build(),
|
||||
RequestBody.fromBytes(createSamplePdf()));
|
||||
|
||||
persisted.setFilePath(pdfKey);
|
||||
persisted = documentRepository.save(persisted);
|
||||
|
||||
ThumbnailService.Outcome outcome = thumbnailService.generate(persisted);
|
||||
|
||||
assertThat(outcome).isEqualTo(ThumbnailService.Outcome.SUCCESS);
|
||||
|
||||
Document reloaded = documentRepository.findById(docId).orElseThrow();
|
||||
assertThat(reloaded.getThumbnailKey()).isEqualTo("thumbnails/" + docId + ".jpg");
|
||||
assertThat(reloaded.getThumbnailGeneratedAt()).isNotNull();
|
||||
|
||||
// Read back from MinIO and verify it decodes as a JPEG of the expected width
|
||||
try (InputStream in = s3Client.getObject(GetObjectRequest.builder()
|
||||
.bucket(BUCKET).key(reloaded.getThumbnailKey()).build())) {
|
||||
byte[] jpegBytes = in.readAllBytes();
|
||||
BufferedImage decoded = ImageIO.read(new ByteArrayInputStream(jpegBytes));
|
||||
assertThat(decoded).isNotNull();
|
||||
assertThat(decoded.getWidth()).isEqualTo(240);
|
||||
}
|
||||
}
|
||||
|
||||
private static S3Client buildClient() {
|
||||
return S3Client.builder()
|
||||
.endpointOverride(URI.create("http://" + minio.getHost() + ":" + minio.getMappedPort(9000)))
|
||||
.serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build())
|
||||
.region(Region.of("eu-central-1"))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(ACCESS_KEY, SECRET_KEY)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static byte[] createSamplePdf() throws IOException {
|
||||
try (PDDocument pdf = new PDDocument()) {
|
||||
pdf.addPage(new PDPage(PDRectangle.A4));
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
pdf.save(bos);
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user