Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ target/
.settings/
*.springBeans
.mvn/wrapper/maven-wrapper.jar
.vscode
.cursor
27 changes: 25 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Whenever a 3rd party library is updated, S3Mock will update it's MINOR version.
* [PLANNED - 5.x - RELEASE TBD](#planned---5x---release-tbd)
* [Planned changes](#planned-changes)
* [CURRENT - 4.x - THIS VERSION IS UNDER ACTIVE DEVELOPMENT](#current---4x---this-version-is-under-active-development)
* [4.6.0 - PLANNED](#460---planned)
* [4.7.0 - PLANNED](#470---planned)
* [4.6.0](#460)
* [4.5.1](#451)
* [4.5.0](#450)
* [4.4.0](#440)
Expand Down Expand Up @@ -146,7 +147,7 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav

**The current major version 4 will receive new features, dependency updates and bug fixes on a continuous basis.**

## 4.6.0 - PLANNED
## 4.7.0 - PLANNED
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.

* Features and fixes
Expand All @@ -158,6 +159,28 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
* Version updates (build dependencies)
* TBD

## 4.6.0
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.

* Features and fixes
* Fail PUT object with match on non-existent keys (fixes #2502)
* Refactorings
* Remove unused imports
* Fix Kotlin 2.2 usage
* Ignore .vscode and .cursor configurations
* Minor refactorings for clarity.
* Use fixed list of StorageClass values in tests. New values added by AWS sometimes break tests. We want to make sure to test a few different storage classes, no need to test every one.
* Version updates (deliverable dependencies)
* Bump aws-v2.version from 2.31.67 to 2.31.77
* Bump testcontainers.version from 1.21.2 to 1.21.3
* Version updates (build dependencies)
* Bump aws.sdk.kotlin:s3-jvm from 1.4.109 to 1.4.119
* Bump org.apache.maven.plugins:maven-gpg-plugin from 3.2.7 to 3.2.8
* Bump org.apache.maven.plugins:maven-enforcer-plugin from 3.5.0 to 3.6.0
* Bump com.puppycrawl.tools:checkstyle from 10.26.0 to 10.26.1
* Bump github/codeql-action from 3.29.1 to 3.29.2
* Bump step-security/harden-runner from 2.12.1 to 2.12.2

## 4.5.1
Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration.

Expand Down
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.PHONY: build verify install
.DEFAULT_GOAL := build

Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider declaring your targets as .PHONY: build verify install to avoid conflicts with files of the same name and ensure they always run as intended.

Suggested change
.PHONY: build verify install

Copilot uses AI. Check for mistakes.
build: verify

verify:
./mvnw -B -V -Dstyle.color=always clean verify

install:
./mvnw -B -V -Dstyle.color=always clean install
2 changes: 1 addition & 1 deletion build-config/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.5.2-SNAPSHOT</version>
<version>4.6.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock-build-config</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion docker/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.5.2-SNAPSHOT</version>
<version>4.6.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock-docker</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.5.2-SNAPSHOT</version>
<version>4.6.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock-integration-tests</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.adobe.testing.s3mock.its

import com.adobe.testing.s3mock.its.S3TestBase.Companion.UPLOAD_FILE
import com.adobe.testing.s3mock.util.DigestUtil
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
Expand Down Expand Up @@ -912,6 +911,21 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
.hasMessageContaining("Service: S3, Status Code: 412")
}

@Test
@S3VerifiedSuccess(year = 2025)
fun `PUT object succeeds with if-none-match on non-existing object`(testInfo: TestInfo) {
val nonMatchingEtag = WILDCARD
val bucketName = givenBucket(testInfo)

s3Client.putObject(
{
it.bucket(bucketName)
it.key(UPLOAD_FILE_NAME)
it.ifNoneMatch(nonMatchingEtag)
}, RequestBody.fromFile(UPLOAD_FILE)
)
}

@Test
@S3VerifiedSuccess(year = 2025)
fun `PUT object fails with if-match=false`(testInfo: TestInfo) {
Expand All @@ -930,6 +944,24 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
.hasMessageContaining("Service: S3, Status Code: 412")
}

@Test
@S3VerifiedSuccess(year = 2025)
fun `PUT object fails with if-match on non-existing object`(testInfo: TestInfo) {
val nonMatchingEtag = "\"$randomName\""
val bucketName = givenBucket(testInfo)

assertThatThrownBy {
s3Client.putObject(
{
it.bucket(bucketName)
it.key(UPLOAD_FILE_NAME)
it.ifMatch(nonMatchingEtag)
}, RequestBody.fromFile(UPLOAD_FILE)
)
}.isInstanceOf(S3Exception::class.java)
.hasMessageContaining("Service: S3, Status Code: 404")
}

@Test
@S3VerifiedFailure(year = 2025,
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,16 +627,14 @@ internal abstract class S3TestBase {

@JvmStatic
protected fun storageClasses(): Stream<StorageClass> {
return StorageClass
.entries
.filter { it != StorageClass.UNKNOWN_TO_SDK_VERSION }
.filter { it != StorageClass.SNOW }
.filter { it != StorageClass.EXPRESS_ONEZONE }
.filter { it != StorageClass.GLACIER }
.filter { it != StorageClass.DEEP_ARCHIVE }
.filter { it != StorageClass.OUTPOSTS }
.map { it }
.stream()
return listOf(
StorageClass.STANDARD,
StorageClass.REDUCED_REDUNDANCY,
StorageClass.STANDARD_IA,
StorageClass.ONEZONE_IA,
StorageClass.INTELLIGENT_TIERING,
StorageClass.GLACIER,
).stream()
}

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2023 Adobe.
* Copyright 2017-2025 Adobe.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -13,22 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.adobe.testing.s3mock.its

package com.adobe.testing.s3mock.its;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtendWith

/**
* A test has been verified as failing against S3 APIs.
* Some tests can't ever run successfully against the S3 API.
* The reason will tell us why.
*/
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RealS3BackendUsedCondition.class)
public @interface S3VerifiedFailure {
String reason();

int year();
}
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(RealS3BackendUsedCondition::class)
annotation class S3VerifiedFailure(val reason: String, val year: Int)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2023 Adobe.
* Copyright 2017-2025 Adobe.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -13,20 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.adobe.testing.s3mock.its

package com.adobe.testing.s3mock.its;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtendWith

/**
* A test has been verified successfully against S3 APIs.
*/
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RealS3BackendUsedCondition.class)
public @interface S3VerifiedSuccess {

int year();

}
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(RealS3BackendUsedCondition::class)
annotation class S3VerifiedSuccess(val year: Int)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2023 Adobe.
* Copyright 2017-2025 Adobe.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -13,12 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.adobe.testing.s3mock.its;
package com.adobe.testing.s3mock.its

/**
* A test is new and needs verification against S3 APIs.
*/
public @interface S3VerifiedTodo {

}
annotation class S3VerifiedTodo
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.5.2-SNAPSHOT</version>
<version>4.6.0-SNAPSHOT</version>
<packaging>pom</packaging>

<name>S3Mock - Parent</name>
Expand Down Expand Up @@ -80,8 +80,8 @@
<java.version>17</java.version>
<kotlin.version>2.2.0</kotlin.version>
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
<kotlin.compiler.apiVersion>2.1</kotlin.compiler.apiVersion>
<kotlin.compiler.languageVersion>2.1</kotlin.compiler.languageVersion>
<kotlin.compiler.apiVersion>2.2</kotlin.compiler.apiVersion>
<kotlin.compiler.languageVersion>2.2</kotlin.compiler.languageVersion>

<aws-v2.version>2.31.67</aws-v2.version>

Expand Down
2 changes: 1 addition & 1 deletion server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-parent</artifactId>
<version>4.5.2-SNAPSHOT</version>
<version>4.6.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.adobe.testing.s3mock;

import com.adobe.testing.s3mock.dto.Region;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,17 +346,24 @@ public void verifyObjectMatching(
@Nullable List<Instant> ifUnmodifiedSince,
@Nullable S3ObjectMetadata s3ObjectMetadata) {
if (s3ObjectMetadata == null) {
// object does not exist, so we can skip the rest of the checks.
// object does not exist,
if (match != null && !match.isEmpty()) {
// client expects an existing object to match a value, but it could not be found.
throw NO_SUCH_KEY;
}
// no client expectations, skip the rest of the checks.
return;
}

var etag = s3ObjectMetadata.etag();
var lastModified = Instant.ofEpochMilli(s3ObjectMetadata.lastModified());

var setModifiedSince = ifModifiedSince != null && !ifModifiedSince.isEmpty();
if (setModifiedSince && ifModifiedSince.get(0).isAfter(lastModified)) {
LOG.debug("Object {} not modified since {}", s3ObjectMetadata.key(), ifModifiedSince.get(0));
throw NOT_MODIFIED;
if (setModifiedSince) {
if (ifModifiedSince.get(0).isAfter(lastModified)) {
LOG.debug("Object {} not modified since {}", s3ObjectMetadata.key(), ifModifiedSince.get(0));
throw NOT_MODIFIED;
}
}

var setMatch = match != null && !match.isEmpty();
Expand All @@ -372,18 +379,20 @@ public void verifyObjectMatching(
}

var setUnmodifiedSince = ifUnmodifiedSince != null && !ifUnmodifiedSince.isEmpty();
if (setUnmodifiedSince && ifUnmodifiedSince.get(0).isBefore(lastModified)) {
LOG.debug("Object {} modified since {}", s3ObjectMetadata.key(), ifUnmodifiedSince.get(0));
throw PRECONDITION_FAILED;
if (setUnmodifiedSince) {
if (ifUnmodifiedSince.get(0).isBefore(lastModified)) {
LOG.debug("Object {} modified since {}", s3ObjectMetadata.key(), ifUnmodifiedSince.get(0));
throw PRECONDITION_FAILED;
}
}

var setNoneMatch = noneMatch != null && !noneMatch.isEmpty();
if (setNoneMatch
&& (noneMatch.contains(WILDCARD_ETAG) || noneMatch.contains(WILDCARD) || noneMatch.contains(etag))
) {
//request cares only that the object etag does not match.
LOG.debug("Object {} does not exist", s3ObjectMetadata.key());
throw NOT_MODIFIED;
if (setNoneMatch) {
if (noneMatch.contains(WILDCARD_ETAG) || noneMatch.contains(WILDCARD) || noneMatch.contains(etag)) {
// request cares only that the object etag does not match.
LOG.debug("Object {} has an ETag {} that matches one of the 'noneMatch' values", s3ObjectMetadata.key(), etag);
throw NOT_MODIFIED;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ BucketStore bucketStore(
bucketStore.createBucket(name,
false,
ObjectOwnership.BUCKET_OWNER_ENFORCED,
region.id(),
mockRegion.id(),
null,
null
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import software.amazon.awssdk.checksums.SdkChecksum;
import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm;
Expand Down
2 changes: 1 addition & 1 deletion testsupport/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>com.adobe.testing</groupId>
<artifactId>s3mock-testsupport-reactor</artifactId>
<version>4.5.2-SNAPSHOT</version>
<version>4.6.0-SNAPSHOT</version>
</parent>

<artifactId>s3mock-testsupport-common</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
import javax.net.ssl.X509ExtendedTrustManager;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
Expand Down
Loading