JAMES-3763 Restores test on deduplicated hashes being encoded in a safe way for all implementations

This is especially important for the S3 implementation which requires blobid
to be valid url path values.

Co-Authored-By: Matthieu Baechler <matthieu@apache.org>
diff --git a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/DeduplicationBlobStoreContract.java b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/DeduplicationBlobStoreContract.java
index a133833..07434b5 100644
--- a/server/blob/blob-api/src/test/java/org/apache/james/blob/api/DeduplicationBlobStoreContract.java
+++ b/server/blob/blob-api/src/test/java/org/apache/james/blob/api/DeduplicationBlobStoreContract.java
@@ -24,10 +24,13 @@
 import static org.apache.james.blob.api.BlobStore.StoragePolicy.SIZE_BASED;
 import static org.apache.james.blob.api.BlobStoreContract.SHORT_BYTEARRAY;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.io.ByteArrayInputStream;
 import java.util.stream.Stream;
 
+import org.junit.jupiter.api.BeforeEach;
+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;
@@ -48,6 +51,22 @@
 
     BlobId.Factory blobIdFactory();
 
+    BlobStore createBlobStore();
+
+    @BeforeEach
+    default void beforeEach() {
+        System.clearProperty("james.blob.id.hash.encoding");
+    }
+
+    @Test
+    default void deduplicationBlobstoreCreationShouldFailOnInvalidProperty() {
+        System.setProperty("james.blob.id.hash.encoding", "invalid");
+
+        assertThatThrownBy(this::createBlobStore)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Unknown encoding type: invalid");
+    }
+
     @ParameterizedTest
     @MethodSource("storagePolicies")
     default void saveShouldReturnBlobIdOfString(BlobStore.StoragePolicy storagePolicy) {
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreClOneTest.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreClOneTest.java
index 772529a..fe49d43 100644
--- a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreClOneTest.java
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreClOneTest.java
@@ -53,12 +53,13 @@
 
 import reactor.core.publisher.Mono;
 
-class CassandraBlobStoreClOneTest implements CassandraBlobStoreContract {
+class CassandraBlobStoreClOneTest implements CassandraBlobStoreContract, DeduplicationBlobStoreContract {
     @RegisterExtension
     static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraBlobModule.MODULE);
 
     private BlobStore testee;
     private CassandraDefaultBucketDAO defaultBucketDAO;
+    private CassandraCluster cassandra;
 
     @BeforeEach
     void setUp(CassandraCluster cassandra) {
@@ -72,17 +73,17 @@
         CassandraBucketDAO bucketDAO = new CassandraBucketDAO(blobIdFactory, this.cassandra.getConf());
         defaultBucketDAO = spy(new CassandraDefaultBucketDAO(this.cassandra.getConf(), blobIdFactory));
         CassandraConfiguration cassandraConfiguration = CassandraConfiguration.builder()
-            .blobPartSize(CHUNK_SIZE)
-            .optimisticConsistencyLevel(true)
-            .build();
+                .blobPartSize(CHUNK_SIZE)
+                .optimisticConsistencyLevel(true)
+                .build();
         MetricFactory metricFactory = metricsTestExtension.getMetricFactory();
-        testee = new MetricableBlobStore(
-            metricFactory,
-            BlobStoreFactory.builder()
-                .blobStoreDAO(new CassandraBlobStoreDAO(defaultBucketDAO, bucketDAO, cassandraConfiguration, BucketName.DEFAULT, metricFactory))
-                .blobIdFactory(blobIdFactory)
-                .defaultBucketName()
-                .deduplication());
+        return new MetricableBlobStore(
+                metricFactory,
+                BlobStoreFactory.builder()
+                        .blobStoreDAO(new CassandraBlobStoreDAO(defaultBucketDAO, bucketDAO, cassandraConfiguration, BucketName.DEFAULT, metricFactory))
+                        .blobIdFactory(blobIdFactory)
+                        .defaultBucketName()
+                        .deduplication());
     }
 
     @Override
diff --git a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreTest.java b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreTest.java
index 2ca4489..c49bfa9 100644
--- a/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreTest.java
+++ b/server/blob/blob-cassandra/src/test/java/org/apache/james/blob/cassandra/CassandraBlobStoreTest.java
@@ -41,6 +41,7 @@
 
     private BlobStore testee;
     private CassandraDefaultBucketDAO defaultBucketDAO;
+    private CassandraCluster cassandra;
 
     @BeforeEach
     void setUp(CassandraCluster cassandra) {
@@ -54,16 +55,16 @@
         CassandraBucketDAO bucketDAO = new CassandraBucketDAO(blobIdFactory, this.cassandra.getConf());
         defaultBucketDAO = spy(new CassandraDefaultBucketDAO(this.cassandra.getConf(), blobIdFactory));
         CassandraConfiguration cassandraConfiguration = CassandraConfiguration.builder()
-            .blobPartSize(CHUNK_SIZE)
-            .build();
+                .blobPartSize(CHUNK_SIZE)
+                .build();
         MetricFactory metricFactory = metricsTestExtension.getMetricFactory();
-        testee = new MetricableBlobStore(
-            metricFactory,
-            BlobStoreFactory.builder()
-                .blobStoreDAO(new CassandraBlobStoreDAO(defaultBucketDAO, bucketDAO, cassandraConfiguration, BucketName.DEFAULT, metricFactory))
-                .blobIdFactory(blobIdFactory)
-                .defaultBucketName()
-                .deduplication());
+        return new MetricableBlobStore(
+                metricFactory,
+                BlobStoreFactory.builder()
+                        .blobStoreDAO(new CassandraBlobStoreDAO(defaultBucketDAO, bucketDAO, cassandraConfiguration, BucketName.DEFAULT, metricFactory))
+                        .blobIdFactory(blobIdFactory)
+                        .defaultBucketName()
+                        .deduplication());
     }
 
     @Override
diff --git a/server/blob/blob-file/src/test/java/org/apache/james/blob/file/FileBlobStoreTest.java b/server/blob/blob-file/src/test/java/org/apache/james/blob/file/FileBlobStoreTest.java
index 2854c7b..ca3d30a 100644
--- a/server/blob/blob-file/src/test/java/org/apache/james/blob/file/FileBlobStoreTest.java
+++ b/server/blob/blob-file/src/test/java/org/apache/james/blob/file/FileBlobStoreTest.java
@@ -34,11 +34,16 @@
 
     @BeforeEach
     void setUp() {
+        blobStore = createBlobStore();
+    }
+
+    @Override
+    public MetricableBlobStore createBlobStore() {
         FileSystemImpl fileSystem = FileSystemImpl.forTesting();
-        blobStore = new MetricableBlobStore(metricsTestExtension.getMetricFactory(), new FileBlobStoreFactory(fileSystem).builder()
-            .blobIdFactory(BLOB_ID_FACTORY)
-            .defaultBucketName()
-            .deduplication());
+        return new MetricableBlobStore(metricsTestExtension.getMetricFactory(), new FileBlobStoreFactory(fileSystem).builder()
+                .blobIdFactory(BLOB_ID_FACTORY)
+                .defaultBucketName()
+                .deduplication());
     }
 
     @Override
diff --git a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreTest.java b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreTest.java
index f678066..7310f0d 100644
--- a/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreTest.java
+++ b/server/blob/blob-memory/src/test/java/org/apache/james/blob/memory/MemoryBlobStoreTest.java
@@ -33,10 +33,15 @@
 
     @BeforeEach
     void setUp() {
-        blobStore = new MetricableBlobStore(metricsTestExtension.getMetricFactory(), MemoryBlobStoreFactory.builder()
-            .blobIdFactory(BLOB_ID_FACTORY)
-            .defaultBucketName()
-            .deduplication());
+        blobStore = createBlobStore();
+    }
+
+    @Override
+    public MetricableBlobStore createBlobStore() {
+        return new MetricableBlobStore(metricsTestExtension.getMetricFactory(), MemoryBlobStoreFactory.builder()
+                .blobIdFactory(BLOB_ID_FACTORY)
+                .defaultBucketName()
+                .deduplication());
     }
 
     @Override
diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java
index 1d4c5d1..728a25d 100644
--- a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java
+++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java
@@ -19,6 +19,11 @@
 
 package org.apache.james.blob.objectstorage.aws;
 
+import static org.apache.james.blob.api.BlobStoreDAOFixture.TEST_BUCKET_NAME;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobStore;
 import org.apache.james.blob.api.BlobStoreContract;
@@ -27,49 +32,65 @@
 import org.apache.james.metrics.api.NoopGaugeRegistry;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
 import org.apache.james.server.blob.deduplication.BlobStoreFactory;
-import org.junit.jupiter.api.AfterAll;
 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 reactor.core.publisher.Flux;
+
 @ExtendWith(DockerAwsS3Extension.class)
-class S3DeDuplicationBlobStoreTest implements BlobStoreContract {
+class S3DeDuplicationBlobStoreTest implements BlobStoreContract, DeduplicationBlobStoreContract {
 
-    private static BlobStore testee;
-    private static S3BlobStoreDAO s3BlobStoreDAO;
-    private static S3ClientFactory s3ClientFactory;
+    private BlobStore testee;
+    private DockerAwsS3Container dockerAwsS3;
+    private S3BlobStoreDAO s3BlobStoreDAO;
+    private S3ClientFactory s3ClientFactory;
 
-    @BeforeAll
-    static void setUpClass(DockerAwsS3Container dockerAwsS3) {
+    @BeforeEach
+    void setUpClass(DockerAwsS3Container dockerAwsS3) {
+        this.dockerAwsS3 = dockerAwsS3;
+        testee = createBlobStore();
+    }
+
+    @Override
+    public BlobStore createBlobStore() {
         AwsS3AuthConfiguration authConfiguration = AwsS3AuthConfiguration.builder()
-            .endpoint(dockerAwsS3.getEndpoint())
-            .accessKeyId(DockerAwsS3Container.ACCESS_KEY_ID)
-            .secretKey(DockerAwsS3Container.SECRET_ACCESS_KEY)
-            .build();
+                .endpoint(dockerAwsS3.getEndpoint())
+                .accessKeyId(DockerAwsS3Container.ACCESS_KEY_ID)
+                .secretKey(DockerAwsS3Container.SECRET_ACCESS_KEY)
+                .build();
 
         S3BlobStoreConfiguration s3Configuration = S3BlobStoreConfiguration.builder()
-            .authConfiguration(authConfiguration)
-            .region(dockerAwsS3.dockerAwsS3().region())
-            .build();
+                .authConfiguration(authConfiguration)
+                .region(dockerAwsS3.dockerAwsS3().region())
+                .build();
 
         HashBlobId.Factory blobIdFactory = new HashBlobId.Factory();
         s3ClientFactory = new S3ClientFactory(s3Configuration, new RecordingMetricFactory(), new NoopGaugeRegistry());
         s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration, blobIdFactory);
 
-        testee = BlobStoreFactory.builder()
-            .blobStoreDAO(s3BlobStoreDAO)
-            .blobIdFactory(blobIdFactory)
-            .defaultBucketName()
-            .deduplication();
+        return BlobStoreFactory.builder()
+                .blobStoreDAO(s3BlobStoreDAO)
+                .blobIdFactory(blobIdFactory)
+                .defaultBucketName()
+                .deduplication();
+    }
+
+    @Test
+    void saveShouldNotGenerateBlobIdValuesThatAreNotValidS3ObjectKeys() {
+        Flux.range(0, 1000)
+                .concatMap(i -> {
+                    byte[] payload = RandomStringUtils.random(128).getBytes(StandardCharsets.UTF_8);
+                    return testee.save(TEST_BUCKET_NAME, payload, BlobStore.StoragePolicy.LOW_COST);
+                })
+                .then()
+                .block();
     }
 
     @AfterEach
     void tearDown() {
         s3BlobStoreDAO.deleteAllBuckets().block();
-    }
-
-    @AfterAll
-    static void tearDownClass() {
         s3ClientFactory.close();
     }
 
diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java
index 62f9f0f..44eafb6 100644
--- a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java
+++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java
@@ -29,14 +29,11 @@
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.net.URI;
-import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
-import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobStoreDAO;
 import org.apache.james.blob.api.BlobStoreDAOContract;
-import org.apache.james.blob.api.HashBlobId;
 import org.apache.james.blob.api.TestBlobId;
 import org.apache.james.metrics.api.NoopGaugeRegistry;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
@@ -48,7 +45,6 @@
 import org.testcontainers.junit.jupiter.Container;
 import org.testcontainers.junit.jupiter.Testcontainers;
 
-import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.util.retry.Retry;
 import software.amazon.awssdk.services.s3.model.S3Exception;
@@ -120,16 +116,4 @@
         Mono.from(testee.save(TEST_BUCKET_NAME, TEST_BLOB_ID, SHORT_BYTEARRAY)).block();
         assertThat(Mono.from(testee.readBytes(TEST_BUCKET_NAME, TEST_BLOB_ID)).block()).isEqualTo(SHORT_BYTEARRAY);
     }
-
-    @Test
-    void saveShouldWorkForAllPayloadsWithHash() {
-        Flux.range(0, 1000)
-            .concatMap(i -> {
-                byte[] payload = RandomStringUtils.random(128).getBytes(StandardCharsets.UTF_8);
-                BlobId blobId = new HashBlobId.Factory().forPayload(payload);
-                return testee.save(TEST_BUCKET_NAME, blobId, payload);
-            })
-            .then()
-            .block();
-    }
 }
diff --git a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java
index 0928b64..10d39c2 100644
--- a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java
+++ b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java
@@ -28,51 +28,53 @@
 import org.apache.james.metrics.api.NoopGaugeRegistry;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
 import org.apache.james.server.blob.deduplication.BlobStoreFactory;
-import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 @ExtendWith(DockerAwsS3Extension.class)
-class S3PrefixAndNamespaceTest implements BlobStoreContract {
-    private static BlobStore testee;
-    private static S3BlobStoreDAO s3BlobStoreDAO;
+class S3PrefixAndNamespaceTest implements BlobStoreContract, DeduplicationBlobStoreContract {
+    private BlobStore testee;
+    private S3BlobStoreDAO s3BlobStoreDAO;
 
-    private static S3ClientFactory s3ClientFactory;
+    private S3ClientFactory s3ClientFactory;
+    private DockerAwsS3Container dockerAwsS3;
 
-    @BeforeAll
-    static void setUpClass(DockerAwsS3Container dockerAwsS3) {
+    @BeforeEach
+    void setUpClass(DockerAwsS3Container dockerAwsS3) {
+        this.dockerAwsS3 = dockerAwsS3;
+        this.testee = createBlobStore();
+    }
+
+    @Override
+    public BlobStore createBlobStore() {
         AwsS3AuthConfiguration authConfiguration = AwsS3AuthConfiguration.builder()
-            .endpoint(dockerAwsS3.getEndpoint())
-            .accessKeyId(DockerAwsS3Container.ACCESS_KEY_ID)
-            .secretKey(DockerAwsS3Container.SECRET_ACCESS_KEY)
-            .build();
+                .endpoint(dockerAwsS3.getEndpoint())
+                .accessKeyId(DockerAwsS3Container.ACCESS_KEY_ID)
+                .secretKey(DockerAwsS3Container.SECRET_ACCESS_KEY)
+                .build();
 
         S3BlobStoreConfiguration s3Configuration = S3BlobStoreConfiguration.builder()
-            .authConfiguration(authConfiguration)
-            .region(dockerAwsS3.dockerAwsS3().region())
-            .defaultBucketName(BucketName.of("namespace"))
-            .bucketPrefix("prefix")
-            .build();
+                .authConfiguration(authConfiguration)
+                .region(dockerAwsS3.dockerAwsS3().region())
+                .defaultBucketName(BucketName.of("namespace"))
+                .bucketPrefix("prefix")
+                .build();
 
         HashBlobId.Factory blobIdFactory = new HashBlobId.Factory();
         s3ClientFactory = new S3ClientFactory(s3Configuration, new RecordingMetricFactory(), new NoopGaugeRegistry());
         s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration, blobIdFactory);
 
-        testee = BlobStoreFactory.builder()
-            .blobStoreDAO(s3BlobStoreDAO)
-            .blobIdFactory(blobIdFactory)
-            .bucket(BucketName.of("namespace"))
-            .deduplication();
+        return BlobStoreFactory.builder()
+                .blobStoreDAO(s3BlobStoreDAO)
+                .blobIdFactory(blobIdFactory)
+                .bucket(BucketName.of("namespace"))
+                .deduplication();
     }
 
     @AfterEach
     void tearDown() {
         s3BlobStoreDAO.deleteAllBuckets().block();
-    }
-
-    @AfterAll
-    static void tearDownClass() {
         s3ClientFactory.close();
     }