From fcfc20694b2ce35a1d7949ed3f1d06f276c504bc Mon Sep 17 00:00:00 2001
From: Hesanda Liyanage <130324291+HesandaLiyanage@users.noreply.github.com>
Date: Wed, 23 Sep 2026 19:27:50 +0530
Subject: [PATCH] [IMPROVEMENT] Zstd: Contract and test coverage for
ZstdBlobStoreDAO atop file and memory implementations
Following PMC feedback regarding ZstdBlobStoreDAO working out of the box atop the file implementation:
- Extract ZstdBlobStoreDAOContract interface extending BlobStoreDAOContract and MetadataAwareBlobStoreDAOContract.
- Add ZstdFileBlobStoreDAOTest verifying ZstdBlobStoreDAO works out of the box atop FileBlobStoreDAO.
- Add ZstdMemoryBlobStoreDAOTest for lightweight in-memory contract verification without requiring Docker.
- Refactor ZstdBlobStoreDAOTest to implement ZstdBlobStoreDAOContract against S3BlobStoreDAO.
- Add File and Postgres BlobStore module choosing tests in BlobStoreModulesChooserTest.
---
server/blob/blob-zstd/pom.xml | 10 +
.../blob/zstd/ZstdBlobStoreDAOContract.java | 387 ++++++++++++++++++
.../james/blob/zstd/ZstdBlobStoreDAOTest.java | 356 +---------------
.../blob/zstd/ZstdFileBlobStoreDAOTest.java | 62 +++
.../blob/zstd/ZstdMemoryBlobStoreDAOTest.java | 60 +++
.../BlobStoreModulesChooserTest.java | 22 +
6 files changed, 550 insertions(+), 347 deletions(-)
create mode 100644 server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOContract.java
create mode 100644 server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdFileBlobStoreDAOTest.java
create mode 100644 server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdMemoryBlobStoreDAOTest.java
diff --git a/server/blob/blob-zstd/pom.xml b/server/blob/blob-zstd/pom.xml
index 053921f2cb8..ccf440ac006 100644
--- a/server/blob/blob-zstd/pom.xml
+++ b/server/blob/blob-zstd/pom.xml
@@ -41,6 +41,16 @@
test-jar
test
+
+ ${james.groupId}
+ blob-file
+ test
+
+
+ ${james.groupId}
+ blob-memory
+ test
+
${james.groupId}
blob-s3
diff --git a/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOContract.java b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOContract.java
new file mode 100644
index 00000000000..245b00ad80e
--- /dev/null
+++ b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOContract.java
@@ -0,0 +1,387 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.blob.zstd;
+
+import static org.apache.james.blob.api.BlobStoreDAOFixture.ELEVEN_KILOBYTES;
+import static org.apache.james.blob.api.BlobStoreDAOFixture.SHORT_BYTEARRAY;
+import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BLOB_ID;
+import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BUCKET_NAME;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Random;
+import java.util.stream.Stream;
+
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.api.BlobStoreDAOContract;
+import org.apache.james.blob.api.MetadataAwareBlobStoreDAOContract;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import com.google.common.io.ByteSource;
+
+import reactor.core.publisher.Mono;
+
+public interface ZstdBlobStoreDAOContract extends BlobStoreDAOContract, MetadataAwareBlobStoreDAOContract {
+
+ BlobStoreDAO underlying();
+
+ RecordingMetricFactory metricFactory();
+
+ @Override
+ ZstdBlobStoreDAO testee();
+
+ default CompressionConfiguration defaultCompressionConfiguration() {
+ return CompressionConfiguration.builder()
+ .enabled(true)
+ .build();
+ }
+
+ @Override
+ @Test
+ default void retrieveContentTransferEncodingShouldSucceed() {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+
+ // should compress and append content-transfer-encoding metadata, when threshold is met.
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
+
+ assertThat(Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block().metadata().contentEncoding())
+ .contains(BlobStoreDAO.ContentEncoding.ZSTD);
+ }
+
+ @Test
+ default void shouldPreserveCallerMetadataWhenCompressionHappens() {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+ BlobStoreDAO.BlobMetadata metadata = BlobStoreDAO.BlobMetadata.empty()
+ .withMetadata(new BlobStoreDAO.BlobMetadataName("name"), new BlobStoreDAO.BlobMetadataValue("value"))
+ .withMetadata(new BlobStoreDAO.BlobMetadataName("type"), new BlobStoreDAO.BlobMetadataValue("attachment"));
+ BlobStoreDAO.BytesBlob blob = BlobStoreDAO.BytesBlob.of(ELEVEN_KILOBYTES.payload(), metadata);
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, blob)).block();
+
+ BlobStoreDAO.BytesBlob readBlob = Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertSoftly(softly -> {
+ softly.assertThat(readBlob.metadata().underlyingMap())
+ .containsEntry(new BlobStoreDAO.BlobMetadataName("name"), new BlobStoreDAO.BlobMetadataValue("value"))
+ .containsEntry(new BlobStoreDAO.BlobMetadataName("type"), new BlobStoreDAO.BlobMetadataValue("attachment"))
+ .containsEntry(BlobStoreDAO.ContentEncoding.NAME, BlobStoreDAO.ContentEncoding.ZSTD.asValue())
+ .containsEntry(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE,
+ new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
+ });
+ }
+
+ @Test
+ default void readExistingNonCompressedBlobShouldSucceed() {
+ Mono.from(underlying().save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
+
+ assertThat(Mono.from(testee().readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block())
+ .isEqualTo(ELEVEN_KILOBYTES);
+ }
+
+ @Test
+ default void shouldNotCompressBlobIfThresholdIsNotMet() {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(16 * 1024)
+ .build(),
+ metricFactory());
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying().readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertSoftly(softly -> {
+ softly.assertThat(storedBlob.payload()).isEqualTo(SHORT_BYTEARRAY.payload());
+ softly.assertThat(storedBlob.metadata().contentEncoding()).isEmpty();
+ softly.assertThat(storedBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE)).isEmpty();
+ });
+ }
+
+ @Test
+ default void readShouldDecompressCompressedBlob() throws IOException {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
+
+ BlobStoreDAO.InputStreamBlob readBlob = localTestee.read(TEST_BUCKET_NAME, TEST_BLOB_ID);
+ byte[] payload = readBlob.payload().readAllBytes();
+
+ assertSoftly(softly -> {
+ softly.assertThat(payload).isEqualTo(ELEVEN_KILOBYTES.payload());
+ softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
+ softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
+ .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
+ });
+ }
+
+ @Test
+ default void readReactiveShouldDecompressCompressedBlob() throws IOException {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
+
+ BlobStoreDAO.InputStreamBlob readBlob = Mono.from(localTestee.readReactive(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+ byte[] payload = readBlob.payload().readAllBytes();
+
+ assertSoftly(softly -> {
+ softly.assertThat(payload).isEqualTo(ELEVEN_KILOBYTES.payload());
+ softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
+ softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
+ .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
+ });
+ }
+
+ @Test
+ default void shouldNotCompressBlobWhenMinRatioIsZero() {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .minRatio(0F)
+ .build(),
+ metricFactory());
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
+
+ BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying().readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertSoftly(softly -> {
+ softly.assertThat(storedBlob.payload()).isEqualTo(ELEVEN_KILOBYTES.payload());
+ softly.assertThat(storedBlob.metadata().contentEncoding()).isEmpty();
+ softly.assertThat(storedBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE)).isEmpty();
+ });
+ }
+
+ @Test
+ default void shouldStillDecompressBlobWhenMinRatioIsZero() {
+ ZstdBlobStoreDAO compressingTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+ ZstdBlobStoreDAO uncompressingOnlyTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .minRatio(0F)
+ .build(),
+ metricFactory());
+
+ Mono.from(compressingTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
+
+ BlobStoreDAO.BytesBlob readBlob = Mono.from(uncompressingOnlyTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertSoftly(softly -> {
+ softly.assertThat(readBlob.payload()).isEqualTo(ELEVEN_KILOBYTES.payload());
+ softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
+ softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
+ .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
+ });
+ }
+
+ @Test
+ default void shouldNotCompressBlobWhenMinRatioIsNotMet() {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .minRatio(0.5F)
+ .build(),
+ metricFactory());
+ byte[] randomPayload = new byte[4096];
+ new Random(1).nextBytes(randomPayload);
+ BlobStoreDAO.BytesBlob randomBlob = BlobStoreDAO.BytesBlob.of(randomPayload);
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, randomBlob)).block();
+
+ BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying().readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertSoftly(softly -> {
+ softly.assertThat(storedBlob.payload()).isEqualTo(randomPayload);
+ softly.assertThat(storedBlob.metadata().contentEncoding()).isEmpty();
+ softly.assertThat(storedBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE)).isEmpty();
+ });
+ }
+
+ @Test
+ default void shouldRecordMetrics() {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
+ Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+ BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying().readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertSoftly(softly -> {
+ softly.assertThat(metricFactory().countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_COMPRESS_SAVE_COUNT_METRIC_NAME)).isEqualTo(1);
+ softly.assertThat(metricFactory().countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_DECOMPRESS_COUNT_METRIC_NAME)).isEqualTo(1);
+ softly.assertThat(metricFactory().countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_SAVED_BYTES_METRIC_NAME))
+ .isEqualTo(ELEVEN_KILOBYTES.payload().length - storedBlob.payload().length);
+ softly.assertThat(metricFactory().executionTimesFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_COMPRESS_LATENCY_METRIC_NAME)).hasSize(1);
+ softly.assertThat(metricFactory().executionTimesFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_DECOMPRESS_LATENCY_METRIC_NAME)).hasSize(1);
+ });
+ }
+
+ @Test
+ default void shouldRecordThresholdSkipMetricIfThresholdNotMatch() {
+ Mono.from(testee().save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
+
+ assertThat(metricFactory().countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_THRESHOLD_SKIP_COUNT_METRIC_NAME))
+ .isEqualTo(1);
+ }
+
+ @ParameterizedTest
+ @MethodSource("blobsWithReservedCompressionMetadata")
+ default void saveShouldRejectReservedCompressionMetadata(BlobStoreDAO.Blob blob) {
+ assertThatThrownBy(() -> Mono.from(testee().save(TEST_BUCKET_NAME, TEST_BLOB_ID, blob)).block())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Reserved zstd metadata are not allowed");
+ }
+
+ @ParameterizedTest
+ @MethodSource("compressionSamples")
+ default void readBytesShouldRoundTripCompressedResourcesWithoutCorruption(String resourcePath) throws IOException {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+ byte[] resourceBytes = readResource(resourcePath);
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, BlobStoreDAO.BytesBlob.of(resourceBytes))).block();
+
+ BlobStoreDAO.BytesBlob readBlob = Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+
+ assertSoftly(softly -> {
+ softly.assertThat(readBlob.payload()).isEqualTo(resourceBytes);
+ softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
+ softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
+ .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(resourceBytes.length)));
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("compressionSamples")
+ default void readShouldRoundTripCompressedResourcesWithoutCorruption(String resourcePath) throws IOException {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+ byte[] resourceBytes = readResource(resourcePath);
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, BlobStoreDAO.BytesBlob.of(resourceBytes))).block();
+
+ BlobStoreDAO.InputStreamBlob readBlob = localTestee.read(TEST_BUCKET_NAME, TEST_BLOB_ID);
+ byte[] payload = readBlob.payload().readAllBytes();
+
+ assertSoftly(softly -> {
+ softly.assertThat(payload).isEqualTo(resourceBytes);
+ softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
+ softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
+ .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(resourceBytes.length)));
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("compressionSamples")
+ default void readReactiveShouldRoundTripCompressedResourcesWithoutCorruption(String resourcePath) throws IOException {
+ ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying(),
+ CompressionConfiguration.builder()
+ .enabled(true)
+ .threshold(1)
+ .build(),
+ metricFactory());
+ byte[] resourceBytes = readResource(resourcePath);
+
+ Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, BlobStoreDAO.BytesBlob.of(resourceBytes))).block();
+
+ BlobStoreDAO.InputStreamBlob readBlob = Mono.from(localTestee.readReactive(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
+ byte[] payload = readBlob.payload().readAllBytes();
+
+ assertSoftly(softly -> {
+ softly.assertThat(payload).isEqualTo(resourceBytes);
+ softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
+ softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
+ .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(resourceBytes.length)));
+ });
+ }
+
+ static Stream compressionSamples() {
+ return Stream.of(
+ Arguments.of("zstd/text.txt"),
+ Arguments.of("zstd/james-logo.jpg"),
+ Arguments.of("zstd/mail1.eml"),
+ Arguments.of("zstd/document.pdf"));
+ }
+
+ static Stream blobsWithReservedCompressionMetadata() {
+ BlobStoreDAO.BlobMetadata reservedMetadata = BlobStoreDAO.BlobMetadata.empty()
+ .withMetadata(BlobStoreDAO.ContentEncoding.NAME, BlobStoreDAO.ContentEncoding.ZSTD.asValue());
+
+ return Stream.of(
+ Arguments.of(BlobStoreDAO.BytesBlob.of(ELEVEN_KILOBYTES.payload(), reservedMetadata)),
+ Arguments.of(BlobStoreDAO.InputStreamBlob.of(new ByteArrayInputStream(ELEVEN_KILOBYTES.payload()), reservedMetadata)),
+ Arguments.of(BlobStoreDAO.ByteSourceBlob.of(ByteSource.wrap(ELEVEN_KILOBYTES.payload()), reservedMetadata)));
+ }
+
+ default byte[] readResource(String resourcePath) throws IOException {
+ try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourcePath)) {
+ assertThat(inputStream).describedAs("resource %s should exist", resourcePath).isNotNull();
+ return inputStream.readAllBytes();
+ }
+ }
+}
diff --git a/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOTest.java b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOTest.java
index 99ddc51af2d..bc2177a3bcd 100644
--- a/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOTest.java
+++ b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdBlobStoreDAOTest.java
@@ -19,28 +19,14 @@
package org.apache.james.blob.zstd;
-import static org.apache.james.blob.api.BlobStoreDAOFixture.ELEVEN_KILOBYTES;
-import static org.apache.james.blob.api.BlobStoreDAOFixture.SHORT_BYTEARRAY;
-import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BLOB_ID;
-import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BUCKET_NAME;
import static org.apache.james.blob.objectstorage.aws.JamesS3MetricPublisher.DEFAULT_S3_METRICS_PREFIX;
import static org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration.UPLOAD_RETRY_EXCEPTION_PREDICATE;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.SoftAssertions.assertSoftly;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
import java.time.Duration;
import java.util.Optional;
-import java.util.Random;
-import java.util.stream.Stream;
import org.apache.james.blob.api.BlobStoreDAO;
-import org.apache.james.blob.api.BlobStoreDAOContract;
import org.apache.james.blob.api.BucketName;
-import org.apache.james.blob.api.MetadataAwareBlobStoreDAOContract;
import org.apache.james.blob.api.TestBlobId;
import org.apache.james.blob.objectstorage.aws.AwsS3AuthConfiguration;
import org.apache.james.blob.objectstorage.aws.DockerAwsS3Container;
@@ -56,22 +42,12 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import com.google.common.io.ByteSource;
-
-import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
@ExtendWith(DockerAwsS3Extension.class)
-class ZstdBlobStoreDAOTest implements BlobStoreDAOContract, MetadataAwareBlobStoreDAOContract {
- private static final CompressionConfiguration DEFAULT_COMPRESSION_CONFIGURATION = CompressionConfiguration.builder()
- .enabled(true)
- .build();
+class ZstdBlobStoreDAOTest implements ZstdBlobStoreDAOContract {
private static final BucketName FALLBACK_BUCKET = BucketName.of("fallback");
private static S3BlobStoreDAO underlying;
@@ -112,7 +88,7 @@ static void tearDownClass() {
@BeforeEach
void setUp() {
metricFactory = new RecordingMetricFactory();
- testee = new ZstdBlobStoreDAO(underlying, DEFAULT_COMPRESSION_CONFIGURATION, metricFactory);
+ testee = new ZstdBlobStoreDAO(underlying, defaultCompressionConfiguration(), metricFactory);
}
@AfterEach
@@ -123,331 +99,17 @@ void tearDown() {
}
@Override
- public BlobStoreDAO testee() {
- return testee;
+ public BlobStoreDAO underlying() {
+ return underlying;
}
@Override
- @Test
- public void retrieveContentTransferEncodingShouldSucceed() {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
-
- // should compress and append content-transfer-encoding metadata, when threshold is met.
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
-
- assertThat(Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block().metadata().contentEncoding())
- .contains(BlobStoreDAO.ContentEncoding.ZSTD);
- }
-
- @Test
- void shouldPreserveCallerMetadataWhenCompressionHappens() {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
- BlobStoreDAO.BlobMetadata metadata = BlobStoreDAO.BlobMetadata.empty()
- .withMetadata(new BlobStoreDAO.BlobMetadataName("name"), new BlobStoreDAO.BlobMetadataValue("value"))
- .withMetadata(new BlobStoreDAO.BlobMetadataName("type"), new BlobStoreDAO.BlobMetadataValue("attachment"));
- BlobStoreDAO.BytesBlob blob = BlobStoreDAO.BytesBlob.of(ELEVEN_KILOBYTES.payload(), metadata);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, blob)).block();
-
- BlobStoreDAO.BytesBlob readBlob = Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
-
- assertSoftly(softly -> {
- softly.assertThat(readBlob.metadata().underlyingMap())
- .containsEntry(new BlobStoreDAO.BlobMetadataName("name"), new BlobStoreDAO.BlobMetadataValue("value"))
- .containsEntry(new BlobStoreDAO.BlobMetadataName("type"), new BlobStoreDAO.BlobMetadataValue("attachment"))
- .containsEntry(BlobStoreDAO.ContentEncoding.NAME, BlobStoreDAO.ContentEncoding.ZSTD.asValue())
- .containsEntry(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE,
- new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
- });
- }
-
- @Test
- void readExistingNonCompressedBlobShouldSucceed() {
- Mono.from(underlying.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
-
- assertThat(Mono.from(testee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block())
- .isEqualTo(ELEVEN_KILOBYTES);
- }
-
- @Test
- void shouldNotCompressBlobIfThresholdIsNotMet() {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(16 * 1024)
- .build(),
- metricFactory);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
-
- BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
-
- assertSoftly(softly -> {
- softly.assertThat(storedBlob.payload()).isEqualTo(SHORT_BYTEARRAY.payload());
- softly.assertThat(storedBlob.metadata().contentEncoding()).isEmpty();
- softly.assertThat(storedBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE)).isEmpty();
- });
- }
-
- @Test
- void readShouldDecompressCompressedBlob() throws IOException {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
-
- BlobStoreDAO.InputStreamBlob readBlob = localTestee.read(TEST_BUCKET_NAME, TEST_BLOB_ID);
- byte[] payload = readBlob.payload().readAllBytes();
-
- assertSoftly(softly -> {
- softly.assertThat(payload).isEqualTo(ELEVEN_KILOBYTES.payload());
- softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
- softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
- .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
- });
- }
-
- @Test
- void readReactiveShouldDecompressCompressedBlob() throws IOException {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
-
- BlobStoreDAO.InputStreamBlob readBlob = Mono.from(localTestee.readReactive(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
- byte[] payload = readBlob.payload().readAllBytes();
-
- assertSoftly(softly -> {
- softly.assertThat(payload).isEqualTo(ELEVEN_KILOBYTES.payload());
- softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
- softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
- .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
- });
- }
-
- @Test
- void shouldNotCompressBlobWhenMinRatioIsZero() {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .minRatio(0F)
- .build(),
- metricFactory);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
-
- BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
-
- assertSoftly(softly -> {
- softly.assertThat(storedBlob.payload()).isEqualTo(ELEVEN_KILOBYTES.payload());
- softly.assertThat(storedBlob.metadata().contentEncoding()).isEmpty();
- softly.assertThat(storedBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE)).isEmpty();
- });
- }
-
- @Test
- void shouldStillDecompressBlobWhenMinRatioIsZero() {
- ZstdBlobStoreDAO compressingTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
- ZstdBlobStoreDAO uncompressingOnlyTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .minRatio(0F)
- .build(),
- metricFactory);
-
- Mono.from(compressingTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
-
- BlobStoreDAO.BytesBlob readBlob = Mono.from(uncompressingOnlyTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
-
- assertSoftly(softly -> {
- softly.assertThat(readBlob.payload()).isEqualTo(ELEVEN_KILOBYTES.payload());
- softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
- softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
- .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(ELEVEN_KILOBYTES.payload().length)));
- });
+ public RecordingMetricFactory metricFactory() {
+ return metricFactory;
}
- @Test
- void shouldNotCompressBlobWhenMinRatioIsNotMet() {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .minRatio(0.5F)
- .build(),
- metricFactory);
- byte[] randomPayload = new byte[4096];
- new Random(1).nextBytes(randomPayload);
- BlobStoreDAO.BytesBlob randomBlob = BlobStoreDAO.BytesBlob.of(randomPayload);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, randomBlob)).block();
-
- BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
-
- assertSoftly(softly -> {
- softly.assertThat(storedBlob.payload()).isEqualTo(randomPayload);
- softly.assertThat(storedBlob.metadata().contentEncoding()).isEmpty();
- softly.assertThat(storedBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE)).isEmpty();
- });
- }
-
- @Test
- void shouldRecordMetrics() {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, ELEVEN_KILOBYTES)).block();
- Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
- BlobStoreDAO.BytesBlob storedBlob = Mono.from(underlying.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
-
- assertSoftly(softly -> {
- softly.assertThat(metricFactory.countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_COMPRESS_SAVE_COUNT_METRIC_NAME)).isEqualTo(1);
- softly.assertThat(metricFactory.countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_DECOMPRESS_COUNT_METRIC_NAME)).isEqualTo(1);
- softly.assertThat(metricFactory.countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_SAVED_BYTES_METRIC_NAME))
- .isEqualTo(ELEVEN_KILOBYTES.payload().length - storedBlob.payload().length);
- softly.assertThat(metricFactory.executionTimesFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_COMPRESS_LATENCY_METRIC_NAME)).hasSize(1);
- softly.assertThat(metricFactory.executionTimesFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_DECOMPRESS_LATENCY_METRIC_NAME)).hasSize(1);
- });
- }
-
- @Test
- void shouldRecordThresholdSkipMetricIfThresholdNotMatch() {
- Mono.from(testee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
-
- assertThat(metricFactory.countFor(ZstdBlobStoreDAO.MetricRecorder.BLOB_ZSTD_THRESHOLD_SKIP_COUNT_METRIC_NAME))
- .isEqualTo(1);
- }
-
- @ParameterizedTest
- @MethodSource("blobsWithReservedCompressionMetadata")
- void saveShouldRejectReservedCompressionMetadata(BlobStoreDAO.Blob blob) {
- assertThatThrownBy(() -> Mono.from(testee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, blob)).block())
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessageContaining("Reserved zstd metadata are not allowed");
- }
-
- @ParameterizedTest
- @MethodSource("compressionSamples")
- void readBytesShouldRoundTripCompressedResourcesWithoutCorruption(String resourcePath) throws IOException {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
- byte[] resourceBytes = readResource(resourcePath);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, BlobStoreDAO.BytesBlob.of(resourceBytes))).block();
-
- BlobStoreDAO.BytesBlob readBlob = Mono.from(localTestee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
-
- assertSoftly(softly -> {
- softly.assertThat(readBlob.payload()).isEqualTo(resourceBytes);
- softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
- softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
- .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(resourceBytes.length)));
- });
- }
-
- @ParameterizedTest
- @MethodSource("compressionSamples")
- void readShouldRoundTripCompressedResourcesWithoutCorruption(String resourcePath) throws IOException {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
- byte[] resourceBytes = readResource(resourcePath);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, BlobStoreDAO.BytesBlob.of(resourceBytes))).block();
-
- BlobStoreDAO.InputStreamBlob readBlob = localTestee.read(TEST_BUCKET_NAME, TEST_BLOB_ID);
- byte[] payload = readBlob.payload().readAllBytes();
-
- assertSoftly(softly -> {
- softly.assertThat(payload).isEqualTo(resourceBytes);
- softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
- softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
- .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(resourceBytes.length)));
- });
- }
-
- @ParameterizedTest
- @MethodSource("compressionSamples")
- void readReactiveShouldRoundTripCompressedResourcesWithoutCorruption(String resourcePath) throws IOException {
- ZstdBlobStoreDAO localTestee = new ZstdBlobStoreDAO(underlying,
- CompressionConfiguration.builder()
- .enabled(true)
- .threshold(1)
- .build(),
- metricFactory);
- byte[] resourceBytes = readResource(resourcePath);
-
- Mono.from(localTestee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, BlobStoreDAO.BytesBlob.of(resourceBytes))).block();
-
- BlobStoreDAO.InputStreamBlob readBlob = Mono.from(localTestee.readReactive(TEST_BUCKET_NAME, TEST_BLOB_ID)).block();
- byte[] payload = readBlob.payload().readAllBytes();
-
- assertSoftly(softly -> {
- softly.assertThat(payload).isEqualTo(resourceBytes);
- softly.assertThat(readBlob.metadata().contentEncoding()).contains(BlobStoreDAO.ContentEncoding.ZSTD);
- softly.assertThat(readBlob.metadata().get(ZstdBlobStoreDAO.CONTENT_ORIGINAL_SIZE))
- .contains(new BlobStoreDAO.BlobMetadataValue(String.valueOf(resourceBytes.length)));
- });
- }
-
- private static Stream compressionSamples() {
- return Stream.of(
- Arguments.of("zstd/text.txt"),
- Arguments.of("zstd/james-logo.jpg"),
- Arguments.of("zstd/mail1.eml"),
- Arguments.of("zstd/document.pdf"));
- }
-
- private static Stream blobsWithReservedCompressionMetadata() {
- BlobStoreDAO.BlobMetadata reservedMetadata = BlobStoreDAO.BlobMetadata.empty()
- .withMetadata(BlobStoreDAO.ContentEncoding.NAME, BlobStoreDAO.ContentEncoding.ZSTD.asValue());
-
- return Stream.of(
- Arguments.of(BlobStoreDAO.BytesBlob.of(ELEVEN_KILOBYTES.payload(), reservedMetadata)),
- Arguments.of(BlobStoreDAO.InputStreamBlob.of(new ByteArrayInputStream(ELEVEN_KILOBYTES.payload()), reservedMetadata)),
- Arguments.of(BlobStoreDAO.ByteSourceBlob.of(ByteSource.wrap(ELEVEN_KILOBYTES.payload()), reservedMetadata)));
- }
-
- private byte[] readResource(String resourcePath) throws IOException {
- try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resourcePath)) {
- assertThat(inputStream).describedAs("resource %s should exist", resourcePath).isNotNull();
- return inputStream.readAllBytes();
- }
+ @Override
+ public ZstdBlobStoreDAO testee() {
+ return testee;
}
}
diff --git a/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdFileBlobStoreDAOTest.java b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdFileBlobStoreDAOTest.java
new file mode 100644
index 00000000000..6532c33c087
--- /dev/null
+++ b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdFileBlobStoreDAOTest.java
@@ -0,0 +1,62 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.blob.zstd;
+
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.api.PlainBlobId;
+import org.apache.james.blob.file.FileBlobStoreDAO;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.server.core.filesystem.FileSystemImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+
+class ZstdFileBlobStoreDAOTest implements ZstdBlobStoreDAOContract {
+
+ private FileBlobStoreDAO underlying;
+ private RecordingMetricFactory metricFactory;
+ private ZstdBlobStoreDAO testee;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ underlying = new FileBlobStoreDAO(FileSystemImpl.forTesting(), new PlainBlobId.Factory());
+ metricFactory = new RecordingMetricFactory();
+ testee = new ZstdBlobStoreDAO(underlying, defaultCompressionConfiguration(), metricFactory);
+ }
+
+ @Override
+ public BlobStoreDAO underlying() {
+ return underlying;
+ }
+
+ @Override
+ public RecordingMetricFactory metricFactory() {
+ return metricFactory;
+ }
+
+ @Override
+ public ZstdBlobStoreDAO testee() {
+ return testee;
+ }
+
+ @Override
+ @Disabled("Not supported on local filesystem due to concurrent file locking")
+ public void mixingSaveReadAndDeleteShouldReturnConsistentState() {
+ }
+}
diff --git a/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdMemoryBlobStoreDAOTest.java b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdMemoryBlobStoreDAOTest.java
new file mode 100644
index 00000000000..e980644f2a2
--- /dev/null
+++ b/server/blob/blob-zstd/src/test/java/org/apache/james/blob/zstd/ZstdMemoryBlobStoreDAOTest.java
@@ -0,0 +1,60 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.blob.zstd;
+
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.memory.MemoryBlobStoreDAO;
+import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+
+class ZstdMemoryBlobStoreDAOTest implements ZstdBlobStoreDAOContract {
+
+ private MemoryBlobStoreDAO underlying;
+ private RecordingMetricFactory metricFactory;
+ private ZstdBlobStoreDAO testee;
+
+ @BeforeEach
+ void setUp() {
+ underlying = new MemoryBlobStoreDAO();
+ metricFactory = new RecordingMetricFactory();
+ testee = new ZstdBlobStoreDAO(underlying, defaultCompressionConfiguration(), metricFactory);
+ }
+
+ @Override
+ public BlobStoreDAO underlying() {
+ return underlying;
+ }
+
+ @Override
+ public RecordingMetricFactory metricFactory() {
+ return metricFactory;
+ }
+
+ @Override
+ public ZstdBlobStoreDAO testee() {
+ return testee;
+ }
+
+ @Override
+ @Disabled("Not supported")
+ public void listBucketsShouldReturnBucketsWithNoBlob() {
+ }
+}
diff --git a/server/container/guice/distributed/src/test/java/org/apache/james/modules/blobstore/BlobStoreModulesChooserTest.java b/server/container/guice/distributed/src/test/java/org/apache/james/modules/blobstore/BlobStoreModulesChooserTest.java
index 53040161e26..8cffb43cbff 100644
--- a/server/container/guice/distributed/src/test/java/org/apache/james/modules/blobstore/BlobStoreModulesChooserTest.java
+++ b/server/container/guice/distributed/src/test/java/org/apache/james/modules/blobstore/BlobStoreModulesChooserTest.java
@@ -49,6 +49,28 @@ void provideBlobStoreShouldReturnCassandraBlobStoreWhenCassandraConfigured() {
.hasSize(1);
}
+ @Test
+ void provideBlobStoreShouldReturnFileBlobStoreWhenFileConfigured() {
+ assertThat(BlobStoreModulesChooser.chooseModules(BlobStoreConfiguration.builder()
+ .file()
+ .disableCache()
+ .passthrough()
+ .noCryptoConfig()))
+ .filteredOn(module -> module instanceof BlobStoreModulesChooser.FileBlobStoreDAODeclarationModule)
+ .hasSize(1);
+ }
+
+ @Test
+ void provideBlobStoreShouldReturnPostgresBlobStoreWhenPostgresConfigured() {
+ assertThat(BlobStoreModulesChooser.chooseModules(BlobStoreConfiguration.builder()
+ .postgres()
+ .disableCache()
+ .passthrough()
+ .noCryptoConfig()))
+ .filteredOn(module -> module instanceof BlobStoreModulesChooser.PostgresBlobStoreDAODeclarationModule)
+ .hasSize(1);
+ }
+
@Test
void provideBlobStoreShouldReturnNoEncryptionWhenNoneConfigured() {
assertThat(BlobStoreModulesChooser.chooseModules(BlobStoreConfiguration.builder()