diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8c8f71677..55d588ef4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,12 +13,10 @@ name: "CodeQL" on: push: - branches: [s3mock-v2, main] + branches: [s3mock-v2, s3mock-v3, s3mock-v4, main] pull_request: # The branches below must be a subset of the branches above - branches: [s3mock-v2, main] - schedule: - - cron: '43 21 * * 6' + branches: [s3mock-v2, s3mock-v3, s3mock-v4, main] # Declare default permissions as read only. permissions: read-all @@ -35,12 +33,14 @@ jobs: actions: read contents: read security-events: write + # CodeQL doesn't support Kotlin 2.3.0 yet (requires < 2.2.30) + # Tracking issue: https://github.com/github/codeql/issues/20661 + continue-on-error: true strategy: fail-fast: false matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + language: [ 'java', 'Kotlin' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: @@ -64,12 +64,12 @@ jobs: # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - # Set up JDK 17, otherwise autobuild will fail below. + # Set up JDK 25, otherwise autobuild will fail below. - name: Set up JDK uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 with: - java-version: 21 - distribution: 'temurin' + java-version: 25 + distribution: 'oracle' cache: 'maven' # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). diff --git a/.github/workflows/maven-ci-and-prb.yml b/.github/workflows/maven-ci-and-prb.yml index a2292866e..836054af7 100644 --- a/.github/workflows/maven-ci-and-prb.yml +++ b/.github/workflows/maven-ci-and-prb.yml @@ -20,9 +20,9 @@ name: Maven Build on: push: - branches: [s3mock-v2, main] + branches: [s3mock-v2, s3mock-v3, s3mock-v4, main] pull_request: - branches: [s3mock-v2, main] + branches: [s3mock-v2, s3mock-v3, s3mock-v4, main] # Declare default permissions as read only. permissions: read-all diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml index 8f0c3fd75..743a8f466 100644 --- a/.github/workflows/maven-release.yml +++ b/.github/workflows/maven-release.yml @@ -40,8 +40,9 @@ jobs: - name: Set up JDK uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 with: - java-version: 21 - distribution: 'temurin' + java-version: 25 + distribution: 'oracle' + cache: 'maven' # The release build pushes a Docker image to Docker Hub, so we need to log in - name: Perform docker login diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c8b44cb0..41cadc184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,7 +117,7 @@ Whenever a 3rd party library is updated, S3Mock will update it's MINOR version. # PLANNED - 6.x - RELEASE TBD Version 6.x is JDK25 LTS bytecode compatible, with Docker integration. -Probably released with Spring Boot 5.x, updating baselines etc. as Spring Boot 5.x requires. +Will be released after Spring Boot 5.x, updating baselines etc. as Spring Boot 5.x requires. Any JUnit / direct Java usage support will most likely be dropped and only supported on a best-effort basis. (i.e., the modules will be deleted from the code base and not released anymore. It *may* be possible to @@ -149,7 +149,13 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav ## 5.0.0 * Features and fixes + * Breaking change (file system): Remove "DisplayName" from Owner. (fixes #2738) + * AWS APIs stopped returning "DisplayName" in November 2025. + * This is unfortunately a breaking change for clients starting S3Mock on existing file systems. * Get object with range now returns the same headers as non-range calls. + * Docker: Copy "s3mock.jar" to "/opt/", run with absolute path reference to avoid issues when working directory is changed. (fixes #2827) + * S3Mock supports ChecksumType.FULL_OBJECT for Multipart uploads (fixes #2843) + * Return 412 on if-none-match=true when making CompleteMultipartRequest (fixes #2790) * Refactorings * Use Jackson 3 annotations and mappers. * AWS has deprecated SDK for Java v1 and will remove support EOY 2025. @@ -159,14 +165,23 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav * Remove legacy properties for S3Mock configuration. * Move all controller-related code from "com.adobe.testing.s3mock" to "com.adobe.testing.s3mock.controller" package. * Remove Apache libraries like "commons-compress", "commons-codec" or "commons-lang3" from dependencies. Kotlin and Java standard library provide similar functionality. -* Version updates - * Bump Spring Boot version to 4.0.0 +* Version updates (deliverable dependencies) + * Bump Spring Boot version to 4.0.1 * Bump Spring Framework version to 7.0.1 - * Bump java version from 17 to 25 - * Compile with Java 25, target Java 17 + * Bump Java version partially from 17 to 25 + * Compile with Java 25, target Java 17. [This follows Spring guidance](https://spring.io/blog/2025/11/13/spring-framework-7-0-general-availability) * Docker container runs Java 25 * Bump TestContainers to 2.0.2 - * Bump Maven to 4.0.0 + * Bump kotlin.version from 2.2.21 to 2.3.0 + * Compile with Kotlin 2.3, target Kotlin 2.2. [This follows Spring guidance](https://spring.io/blog/2025/12/18/next-level-kotlin-support-in-spring-boot-4#kotlin-2-baseline) +* Version updates (build dependencies) + * Bump Maven to 4.0.0-rc5 (TODO: update to 4.0.0) + * Bump org.apache.maven.plugins:maven-release-plugin from 3.3.0 to 3.3.1 + * Bump com.puppycrawl.tools:checkstyle from 12.2.0 to 12.3.0 + * Bump actions/upload-artifact from 5.0.0 to 6.0.0 + * Bump github/codeql-action from 4.31.6 to 4.31.9 + * Bump actions/setup-java from 5.0.0 to 5.1.0 + * Bump step-security/harden-runner from 2.13.3 to 2.14.0 # DEPRECATED - 4.x Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration. diff --git a/Makefile b/Makefile index 7542bc9d6..3eeb0c3b5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # -# Copyright 2017-2025 Adobe. +# Copyright 2017-2026 Adobe. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # limitations under the License. # -.PHONY: build verify install +.PHONY: build verify install sort .DEFAULT_GOAL := build build: verify @@ -24,3 +24,6 @@ verify: install: ./mvnw -B -V -Dstyle.color=always clean install + +sort: + ./mvnw -B -V -Dstyle.color=always com.github.ekryd.sortpom:sortpom-maven-plugin:sort diff --git a/docker/Dockerfile b/docker/Dockerfile index 5f39f9a2f..6a5c058fd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -59,7 +59,7 @@ ENV JAVA_HOME=/opt/java-minimal ENV PATH="$PATH:$JAVA_HOME/bin" COPY --from=staging_area "$JAVA_HOME" "$JAVA_HOME" -COPY ./target/s3mock-exec.jar s3mock.jar +COPY ./target/s3mock-exec.jar /opt/s3mock.jar ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en @@ -69,4 +69,4 @@ ENV root=/s3mockroot EXPOSE 9090 9191 # run the app on startup -ENTRYPOINT ["java", "--illegal-access=warn", "-Djava.security.egd=file:/dev/./urandom", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "s3mock.jar" ] +ENTRYPOINT ["java", "--illegal-access=warn", "-Djava.security.egd=file:/dev/./urandom", "-XX:+UseZGC", "-XX:+ZGenerational", "-jar", "/opt/s3mock.jar" ] diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 28bcdeae1..eb3b5db10 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -30,6 +30,11 @@ S3Mock - Integration Tests + + aws.sdk.kotlin + s3-jvm + test + com.adobe.testing s3mock @@ -48,6 +53,21 @@ + + org.apache.httpcomponents + httpclient + test + + + org.apache.httpcomponents + httpcore + test + + + org.apache.httpcomponents + httpmime + test + org.assertj assertj-core @@ -58,6 +78,12 @@ awaitility test + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + test + org.jetbrains.kotlin @@ -67,8 +93,7 @@ org.jetbrains.kotlin - kotlin-reflect - ${kotlin.version} + kotlin-test-junit test @@ -84,11 +109,6 @@ ${kotlin-coroutines.version} test - - org.jetbrains.kotlin - kotlin-test-junit - test - org.junit.jupiter junit-jupiter-api @@ -111,27 +131,22 @@ software.amazon.awssdk - aws-query-protocol - test - - - software.amazon.awssdk - aws-xml-protocol + aws-crt-client test software.amazon.awssdk - s3 + aws-query-protocol test software.amazon.awssdk - url-connection-client + aws-xml-protocol test software.amazon.awssdk - aws-crt-client + s3 test @@ -140,23 +155,8 @@ test - aws.sdk.kotlin - s3-jvm - test - - - org.apache.httpcomponents - httpclient - test - - - org.apache.httpcomponents - httpcore - test - - - org.apache.httpcomponents - httpmime + software.amazon.awssdk + url-connection-client test @@ -172,7 +172,6 @@ - src/test/kotlin @@ -212,13 +211,13 @@ - - maven-checkstyle-plugin - com.github.gantsign.maven ktlint-maven-plugin + + maven-checkstyle-plugin + org.apache.maven.plugins maven-surefire-plugin @@ -228,11 +227,12 @@ - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + src/test/kotlin diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt index d05ec86a7..530f89067 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt @@ -63,7 +63,6 @@ internal class AclIT : S3TestBase() { }.also { resp -> assertThat(resp.sdkHttpResponse().isSuccessful).isTrue() assertThat(resp.owner().id()).isNotBlank() - assertThat(resp.owner().displayName()).isNotBlank() assertThat(resp.grants()).hasSize(1) assertThat(resp.grants().first().permission()).isEqualTo(FULL_CONTROL) } @@ -86,7 +85,6 @@ internal class AclIT : S3TestBase() { acl.owner().also { owner -> assertThat(owner.id()).isEqualTo(DEFAULT_OWNER.id) - assertThat(owner.displayName()).isEqualTo(DEFAULT_OWNER.displayName) } acl.grants().also { @@ -102,7 +100,6 @@ internal class AclIT : S3TestBase() { .also { grantee -> assertThat(grantee).isNotNull assertThat(grantee.id()).isEqualTo(DEFAULT_OWNER.id) - assertThat(grantee.displayName()).isEqualTo(DEFAULT_OWNER.displayName) assertThat(grantee.type()).isEqualTo(CANONICAL_USER) } } @@ -126,7 +123,6 @@ internal class AclIT : S3TestBase() { it.accessControlPolicy { it.owner { it.id(userId) - it.displayName(userName) } it .grants( @@ -150,7 +146,6 @@ internal class AclIT : S3TestBase() { acl.owner().also { assertThat(it).isNotNull assertThat(it.id()).isEqualTo(userId) - assertThat(it.displayName()).isEqualTo(userName) } assertThat(acl.grants()).hasSize(1) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt index 723370e39..9b87e896b 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt @@ -167,7 +167,6 @@ internal class BucketIT : S3TestBase() { } assertThat(it.prefix()).isNull() assertThat(it.continuationToken()).isNull() - assertThat(it.owner().displayName()).isEqualTo("s3-mock-file-store") assertThat(it.owner().id()).isEqualTo("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") } } @@ -205,7 +204,6 @@ internal class BucketIT : S3TestBase() { } assertThat(it.prefix()).isEqualTo(bucketName) assertThat(it.continuationToken()).isNull() - assertThat(it.owner().displayName()).isEqualTo("s3-mock-file-store") assertThat(it.owner().id()).isEqualTo("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") } } @@ -245,7 +243,6 @@ internal class BucketIT : S3TestBase() { } assertThat(it.prefix()).isNull() assertThat(it.continuationToken()).isNotNull - assertThat(it.owner().displayName()).isEqualTo("s3-mock-file-store") assertThat(it.owner().id()).isEqualTo("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") }.continuationToken() @@ -263,7 +260,6 @@ internal class BucketIT : S3TestBase() { } assertThat(it.prefix()).isNull() assertThat(it.continuationToken()).isNull() - assertThat(it.owner().displayName()).isEqualTo("s3-mock-file-store") assertThat(it.owner().id()).isEqualTo("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") } } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt index db31228b5..9cf631f75 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt @@ -536,7 +536,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() { ) }.also { assertThat(it.eTag()).isEqualTo(eTag.trim('"')) - // default storageClass is STANDARD, which is never returned from APIs except by GetObjectAttributes + // GetObjectAttributes returns the default storageClass "STANDARD", even though other APIs may not. assertThat(it.storageClass()).isEqualTo(StorageClass.STANDARD) assertThat(it.objectSize()).isEqualTo(UPLOAD_FILE_LENGTH) assertThat(it.checksum().checksumSHA1()).isEqualTo(expectedChecksum) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt index 766e1e7c6..d4af7fbb9 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt @@ -29,6 +29,7 @@ import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm import software.amazon.awssdk.services.s3.model.CommonPrefix import software.amazon.awssdk.services.s3.model.EncodingType import software.amazon.awssdk.services.s3.model.NoSuchBucketException +import software.amazon.awssdk.services.s3.model.ObjectStorageClass import software.amazon.awssdk.services.s3.model.S3Object import software.amazon.awssdk.utils.http.SdkHttpUtils @@ -103,6 +104,14 @@ internal class ListObjectsIT : S3TestBase() { Tuple(arrayListOf(ChecksumAlgorithm.SHA256)), Tuple(arrayListOf(ChecksumAlgorithm.SHA256)), ) + // ListObjects returns the default storageClass "STANDARD", even though other APIs may not. + assertThat(it.contents()) + .hasSize(2) + .extracting(S3Object::storageClass) + .containsOnly( + Tuple(ObjectStorageClass.STANDARD), + Tuple(ObjectStorageClass.STANDARD), + ) } } @@ -168,6 +177,8 @@ internal class ListObjectsIT : S3TestBase() { listing.contents().also { assertThat(it).hasSize(1) assertThat(it[0].key()).isEqualTo(key) + // ListObjectsV2 returns the default storageClass "STANDARD", even though other APIs may not. + assertThat(it[0].storageClass()).isEqualTo(ObjectStorageClass.STANDARD) } } } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt index 732c03a0c..7ba5cab42 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt @@ -187,6 +187,174 @@ internal class MultipartIT : S3TestBase() { } } + @Test + @S3VerifiedSuccess(year = 2025) + fun testMultipartUpload_withChecksumType_COMPOSITE(testInfo: TestInfo) { + val bucketName = givenBucket(testInfo) + val initiateMultipartUploadResult = + s3Client + .createMultipartUpload { + it.bucket(bucketName) + it.key(UPLOAD_FILE_NAME) + it.checksumAlgorithm(ChecksumAlgorithm.CRC32) + it.checksumType(ChecksumType.COMPOSITE) + } + val uploadId = initiateMultipartUploadResult.uploadId() + val uploadPartResult = + s3Client.uploadPart( + { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(uploadId) + it.partNumber(1) + it.checksumAlgorithm(ChecksumAlgorithm.CRC32) + it.contentLength(UPLOAD_FILE_LENGTH) + }, + RequestBody.fromFile(UPLOAD_FILE), + ) + + val checksum = + DigestUtil.checksumMultipart( + listOf(UPLOAD_FILE_PATH), + DefaultChecksumAlgorithm.CRC32, + ) + + s3Client + .completeMultipartUpload { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(initiateMultipartUploadResult.uploadId()) + it.checksumType(ChecksumType.COMPOSITE) + it.multipartUpload { + it.parts({ + it.eTag(uploadPartResult.eTag()) + it.partNumber(1) + it.checksumCRC32(uploadPartResult.checksumCRC32()) + }) + } + }.also { + assertThat(it.checksumCRC32()).isEqualTo(checksum) + } + + val etag = "\"${DigestUtil.hexDigestMultipart(listOf(UPLOAD_FILE_PATH))}\"" + s3Client + .getObject { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.checksumMode(ChecksumMode.ENABLED) + }.use { + assertThat(it.response().eTag()).isEqualTo(etag) + assertThat(it.response().checksumCRC32()).isEqualTo(checksum) + } + } + + @Test + @S3VerifiedSuccess(year = 2025) + fun testMultipartUpload_withChecksumType_throwsOn_DIFFERENT(testInfo: TestInfo) { + val bucketName = givenBucket(testInfo) + val initiateMultipartUploadResult = + s3Client + .createMultipartUpload { + it.bucket(bucketName) + it.key(UPLOAD_FILE_NAME) + it.checksumAlgorithm(ChecksumAlgorithm.CRC32) + it.checksumType(ChecksumType.COMPOSITE) + } + val uploadId = initiateMultipartUploadResult.uploadId() + val uploadPartResult = + s3Client.uploadPart( + { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(uploadId) + it.partNumber(1) + it.checksumAlgorithm(ChecksumAlgorithm.CRC32) + it.contentLength(UPLOAD_FILE_LENGTH) + }, + RequestBody.fromFile(UPLOAD_FILE), + ) + + assertThatThrownBy { + s3Client + .completeMultipartUpload { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(initiateMultipartUploadResult.uploadId()) + it.checksumType(ChecksumType.FULL_OBJECT) // intentionally different from creteMultipartUpload value + it.multipartUpload { + it.parts({ + it.eTag(uploadPartResult.eTag()) + it.partNumber(1) + it.checksumCRC32(uploadPartResult.checksumCRC32()) + }) + } + } + }.isInstanceOf(AwsServiceException::class.java) + .hasMessageContaining("Service: S3, Status Code: 400") + } + + @Test + @S3VerifiedSuccess(year = 2025) + fun testMultipartUpload_withChecksumType_FULL_OBJECT(testInfo: TestInfo) { + val bucketName = givenBucket(testInfo) + val initiateMultipartUploadResult = + s3Client + .createMultipartUpload { + it.bucket(bucketName) + it.key(UPLOAD_FILE_NAME) + it.checksumAlgorithm(ChecksumAlgorithm.CRC64_NVME) + it.checksumType(ChecksumType.FULL_OBJECT) + } + val uploadId = initiateMultipartUploadResult.uploadId() + val uploadPartResult = + s3Client.uploadPart( + { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(uploadId) + it.partNumber(1) + it.checksumAlgorithm(ChecksumAlgorithm.CRC64_NVME) + it.contentLength(UPLOAD_FILE_LENGTH) + }, + RequestBody.fromFile(UPLOAD_FILE), + ) + + val checksum = + DigestUtil.checksumFor( + UPLOAD_FILE_PATH, + DefaultChecksumAlgorithm.CRC64NVME, + ) + + s3Client + .completeMultipartUpload { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(initiateMultipartUploadResult.uploadId()) + it.checksumType(ChecksumType.FULL_OBJECT) + it.multipartUpload { + it.parts({ + it.eTag(uploadPartResult.eTag()) + it.partNumber(1) + it.checksumCRC64NVME(uploadPartResult.checksumCRC64NVME()) + }) + } + }.also { + assertThat(it.checksumCRC64NVME()).isEqualTo(checksum) + } + + val etag = "\"${DigestUtil.hexDigestMultipart(listOf(UPLOAD_FILE_PATH))}\"" + + s3Client + .getObject { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.checksumMode(ChecksumMode.ENABLED) + }.use { + assertThat(it.response().eTag()).isEqualTo(etag) + assertThat(it.response().checksumCRC64NVME()).isEqualTo(checksum) + } + } + /** * Tests if a multipart upload with the last part being smaller than 5MB works. */ @@ -329,6 +497,7 @@ internal class MultipartIT : S3TestBase() { it.bucket(initiateMultipartUploadResult.bucket()) it.key(initiateMultipartUploadResult.key()) it.uploadId(initiateMultipartUploadResult.uploadId()) + it.checksumType(ChecksumType.COMPOSITE) it.multipartUpload { it.parts( { @@ -373,6 +542,7 @@ internal class MultipartIT : S3TestBase() { it.bucket(initiateMultipartUploadResult.bucket()) it.key(initiateMultipartUploadResult.key()) it.uploadId(initiateMultipartUploadResult.uploadId()) + it.checksumType(ChecksumType.COMPOSITE) it.multipartUpload { it.parts( { @@ -395,7 +565,6 @@ internal class MultipartIT : S3TestBase() { assertThat(completeMultipartUpload.bucket()).isEqualTo(completeMultipartUpload1.bucket()) assertThat(completeMultipartUpload.key()).isEqualTo(completeMultipartUpload1.key()) assertThat(completeMultipartUpload.eTag()).isEqualTo(completeMultipartUpload1.eTag()) - assertThat(completeMultipartUpload.checksumCRC32()).isEqualTo(completeMultipartUpload1.checksumCRC32()) assertThat(completeMultipartUpload.checksumType()).isEqualTo(completeMultipartUpload1.checksumType()) } @@ -1644,7 +1813,7 @@ internal class MultipartIT : S3TestBase() { it.sourceKey(sourceKey) it.sourceBucket(bucketName) it.partNumber(1) - it.copySourceRange("bytes=0-$UPLOAD_FILE_LENGTH") + it.copySourceRange("bytes=0-${UPLOAD_FILE_LENGTH - 1}") it.copySourceIfModifiedSince(now) } }.isInstanceOf(S3Exception::class.java) @@ -1913,6 +2082,54 @@ internal class MultipartIT : S3TestBase() { .hasMessageContaining(INVALID_PART) } + @Test + @S3VerifiedSuccess(year = 2025) + fun `CompleteMultipart fails with if-none-match=true`(testInfo: TestInfo) { + val (bucketName, response) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME) + val initiateMultipartUploadResult = + s3Client + .createMultipartUpload { + it.bucket(bucketName) + it.key(UPLOAD_FILE_NAME) + } + val uploadId = initiateMultipartUploadResult.uploadId() + + assertThat( + s3Client + .listMultipartUploads { + it.bucket(bucketName) + }.uploads(), + ).isNotEmpty + + val eTag = + s3Client + .uploadPart( + { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(uploadId) + it.partNumber(1) + }, + RequestBody.fromFile(UPLOAD_FILE), + ).eTag() + + assertThatThrownBy { + s3Client.completeMultipartUpload { + it.bucket(initiateMultipartUploadResult.bucket()) + it.key(initiateMultipartUploadResult.key()) + it.uploadId(uploadId) + it.ifNoneMatch("*") + it.multipartUpload { + it.parts({ + it.eTag(eTag) + it.partNumber(1) + }) + } + } + }.isInstanceOf(S3Exception::class.java) + .hasMessageContaining(PRECONDITION_FAILED.message) + } + private fun uploadPart( bucketName: String, key: String, @@ -1939,7 +2156,7 @@ internal class MultipartIT : S3TestBase() { private const val NO_SUCH_BUCKET = "The specified bucket does not exist" private const val INVALID_PART_NUMBER = "Part number must be an integer between 1 and 10000, inclusive" private const val INVALID_PART = - "One or more of the specified parts could not be found. " + - "The part might not have been uploaded, or the specified entity tag may not match the part's entity tag." + "One or more of the specified parts could not be found. " + + "The part may not have been uploaded, or the specified entity tag may not match the part's entity tag." } } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt index 3527d8111..bdfc25e0a 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt @@ -27,6 +27,7 @@ import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.BucketVersioningStatus import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm import software.amazon.awssdk.services.s3.model.ObjectAttributes +import software.amazon.awssdk.services.s3.model.ObjectVersionStorageClass import software.amazon.awssdk.services.s3.model.S3Exception import software.amazon.awssdk.services.s3.model.StorageClass @@ -100,7 +101,7 @@ internal class VersionsIT : S3TestBase() { ) }.also { assertThat(it.versionId()).isEqualTo(versionId) - // default storageClass is STANDARD, which is never returned from APIs + // GetObjectAttributes returns the default storageClass "STANDARD", even though other APIs may not. assertThat(it.storageClass()).isEqualTo(StorageClass.STANDARD) assertThat(it.objectSize()).isEqualTo(UPLOAD_FILE_LENGTH) assertThat(it.checksum().checksumSHA1()).isEqualTo(expectedChecksum) @@ -260,6 +261,8 @@ internal class VersionsIT : S3TestBase() { assertThat(listObjectVersions.hasVersions()).isTrue assertThat(listObjectVersions.versions()[0].key()).isEqualTo(UPLOAD_FILE_NAME) assertThat(listObjectVersions.versions()[0].versionId()).isEqualTo(versionId) + // ListObjectVersions returns the default storageClass "STANDARD", even though other APIs may not. + assertThat(listObjectVersions.versions()[0].storageClass()).isEqualTo(ObjectVersionStorageClass.STANDARD) assertThatThrownBy { s3Client.getObject { diff --git a/pom.xml b/pom.xml index 05e88259e..7a43f4d02 100644 --- a/pom.xml +++ b/pom.xml @@ -76,30 +76,33 @@ - 25 - 17 - 2.3.0 - ${java.target.version} - 2.2 - 2.2 - 1.10.2 - - 2.40.0 1.5.95 - 4.5.14 - 4.5.14 - 4.4.16 + 2.40.0 + + 12.3.0 s3mock-buildx 0.48.0 adobe/s3mock - 1.0.0 + 2.1.0 + 3.6.2 + + 4.5.14 + 4.4.16 + 4.5.14 + 17 + 25 + 26.0.2-1 5.7.2 4.13.2 - - 12.3.0 + 1.10.2 + 2.2 + ${java.target.version} + 2.2 + 2.3.0 + 3.5.0 5.0.0 3.6.0 3.5.0 @@ -116,7 +119,6 @@ 3.4.0 3.4.0 3.5.4 - 3.6.2 6.1.0 1.7.0 UTF-8 @@ -125,76 +127,14 @@ false 4.0.0 4.0.1 + 0.0.14 2.0.2 7.11.0 2.11.0 - 0.0.14 - 3.5.0 - 26.0.2-1 - - com.adobe.testing - s3mock - ${project.version} - - - com.adobe.testing - s3mock - ${project.version} - exec - - - - com.adobe.testing - s3mock-docker - ${project.version} - pom - - - com.adobe.testing - s3mock-testsupport-common - ${project.version} - - - junit - junit - ${junit.version} - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - org.jetbrains.kotlin - kotlin-reflect - ${kotlin.version} - - - org.jetbrains.kotlin - kotlin-test - ${kotlin.version} - - - org.jetbrains.kotlin - kotlin-test-junit - ${kotlin.version} - - - org.mockito.kotlin - mockito-kotlin - ${mockito-kotlin.version} - - - org.springframework.boot - spring-boot-starter-parent - ${spring-boot.version} - pom - import - org.jetbrains.kotlin kotlin-bom @@ -210,74 +150,46 @@ import - org.testcontainers - testcontainers-bom - ${testcontainers.version} - - - org.testng - testng - ${testng.version} - - - org.xmlunit - xmlunit-assertj3 - ${xmlunit-assertj3.version} - - - software.amazon.awssdk - aws-query-protocol - ${aws-v2.version} - - - software.amazon.awssdk - aws-xml-protocol - ${aws-v2.version} - - - software.amazon.awssdk - s3 - ${aws-v2.version} - - - software.amazon.awssdk - url-connection-client - ${aws-v2.version} - - - software.amazon.awssdk - aws-crt-client - ${aws-v2.version} + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + pom + import software.amazon.awssdk - regions + bom ${aws-v2.version} + pom + import - software.amazon.awssdk - utils - ${aws-v2.version} + aws.sdk.kotlin + s3-jvm + ${aws-kotlin.version} - software.amazon.awssdk - auth - ${aws-v2.version} + com.adobe.testing + s3mock + ${project.version} - software.amazon.awssdk - checksums - ${aws-v2.version} + com.adobe.testing + s3mock + ${project.version} + exec + - software.amazon.awssdk - s3-transfer-manager - ${aws-v2.version} + com.adobe.testing + s3mock-docker + ${project.version} + pom - aws.sdk.kotlin - s3-jvm - ${aws-kotlin.version} + com.adobe.testing + s3mock-testsupport-common + ${project.version} org.apache.httpcomponents @@ -294,11 +206,6 @@ httpmime ${httpmime.version} - - org.jspecify - jspecify - ${jspecify.version} - org.jetbrains @@ -306,9 +213,24 @@ ${jetbrains-annotations.version} - org.mockito - mockito-core - test + org.mockito.kotlin + mockito-kotlin + ${mockito-kotlin.version} + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + + + org.testng + testng + ${testng.version} + + + org.xmlunit + xmlunit-assertj3 + ${xmlunit-assertj3.version} digital.pragmatech.testing @@ -316,17 +238,17 @@ ${spring-test-profiler.version} test + + org.mockito + mockito-core + test + - - org.apache.maven.plugins - maven-dependency-plugin - ${maven-dependency-plugin.version} - com.github.ekryd.sortpom sortpom-maven-plugin @@ -335,17 +257,32 @@ ${project.build.sourceEncoding} true recommended_2008_06 - groupId,artifactId,scope + scope,groupId,artifactId true - groupId,artifactId,scope + scope,groupId,artifactId true + false sort - verify + + + + + com.github.gantsign.maven + ktlint-maven-plugin + ${ktlint-maven-plugin.version} + + + check-style + + + + check + @@ -439,21 +376,6 @@ - - com.github.gantsign.maven - ktlint-maven-plugin - ${ktlint-maven-plugin.version} - - - check-style - - - - check - - - - maven-clean-plugin ${maven-clean-plugin.version} @@ -497,6 +419,11 @@ + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + maven-deploy-plugin ${maven-deploy-plugin.version} @@ -512,8 +439,8 @@ ${java.version} - - + + @@ -622,6 +549,25 @@ exec-maven-plugin ${exec-maven-plugin.version} + + org.jetbrains.dokka + dokka-maven-plugin + ${dokka-maven-plugin.version} + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + + javadocJar + + prepare-package + + + + + + spring + + org.jetbrains.kotlin @@ -641,16 +598,6 @@ ${kotlin.version} - true - - true - - -Xjsr305=strict - - - spring - - compile @@ -713,10 +660,11 @@ - maven-javadoc-plugin + maven-source-plugin - maven-source-plugin + org.jetbrains.dokka + dokka-maven-plugin org.sonatype.plugins diff --git a/server/pom.xml b/server/pom.xml index 63ff4f880..64cbc9259 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -35,29 +35,40 @@ + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib + org.springframework.boot - spring-boot-starter-web + spring-boot-configuration-processor + true - tools.jackson.dataformat - jackson-dataformat-xml + org.springframework.boot + spring-boot-devtools + true - tools.jackson.core - jackson-databind + org.springframework.boot + spring-boot-starter-actuator + true - tools.jackson.module - jackson-module-kotlin + org.springframework.boot + spring-boot-starter-web software.amazon.awssdk - regions + aws-crt-client software.amazon.awssdk - utils + regions software.amazon.awssdk @@ -76,19 +87,24 @@ software.amazon.awssdk - aws-crt-client + utils - org.jspecify - jspecify + tools.jackson.core + jackson-databind - org.jetbrains.kotlin - kotlin-stdlib + tools.jackson.dataformat + jackson-dataformat-xml - org.jetbrains.kotlin - kotlin-reflect + tools.jackson.module + jackson-module-kotlin + + + digital.pragmatech.testing + spring-test-profiler + test @@ -97,13 +113,13 @@ test - software.amazon.awssdk - auth + org.glassfish.jaxb + jaxb-runtime test - org.glassfish.jaxb - jaxb-runtime + org.jetbrains.kotlin + kotlin-test-junit test @@ -117,19 +133,14 @@ test - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-devtools - true + org.mockito.kotlin + mockito-kotlin + test org.springframework.boot - spring-boot-starter-actuator - true + spring-boot-starter-restclient + test org.springframework.boot @@ -141,41 +152,31 @@ spring-boot-webmvc-test test - - org.springframework.boot - spring-boot-starter-restclient - test - org.xmlunit xmlunit-assertj3 test - org.jetbrains.kotlin - kotlin-test-junit - test - - - org.mockito.kotlin - mockito-kotlin - test - - - digital.pragmatech.testing - spring-test-profiler + software.amazon.awssdk + auth test + + com.github.gantsign.maven + ktlint-maven-plugin + maven-checkstyle-plugin - com.github.gantsign.maven - ktlint-maven-plugin + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} org.springframework.boot @@ -192,11 +193,6 @@ - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/S3Exception.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/S3Exception.kt index 8ce571fc4..4a6c051c5 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/S3Exception.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/S3Exception.kt @@ -20,12 +20,12 @@ import org.springframework.http.HttpStatus /** * [RuntimeException] to communicate general S3 errors. * These are handled by ControllerConfiguration.S3MockExceptionHandler, - * mapped to [ErrorResponse] and serialized. + * mapped to [com.adobe.testing.s3mock.dto.ErrorResponse] and serialized. * [API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html) */ class S3Exception /** - * Creates a new S3Exception to be mapped as an [ErrorResponse]. + * Creates a new S3Exception to be mapped as an [com.adobe.testing.s3mock.dto.ErrorResponse]. * * @param status The Error Status. * @param code The Error Code. @@ -43,7 +43,7 @@ class S3Exception ) val INVALID_PART: S3Exception = S3Exception( HttpStatus.BAD_REQUEST.value(), "InvalidPart", - "One or more of the specified parts could not be found. The part might not have been " + "One or more of the specified parts could not be found. The part may not have been " + "uploaded, or the specified entity tag may not match the part's entity tag." ) val INVALID_PART_ORDER: S3Exception = S3Exception( @@ -67,6 +67,14 @@ class S3Exception ) } + fun completeRequestWrongChecksumMode(checksumMode: String): S3Exception { + return S3Exception( + HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_CODE, + ("The upload was created using the $checksumMode checksum mode. " + + "The complete request must use the same checksum mode.") + ) + } + val NO_SUCH_UPLOAD_MULTIPART: S3Exception = S3Exception( HttpStatus.NOT_FOUND.value(), "NoSuchUpload", "The specified multipart upload does not exist. The upload ID might be invalid, or the " diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/S3MockProperties.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/S3MockProperties.kt index 50eb27647..55259a4b6 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/S3MockProperties.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/S3MockProperties.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2025 Adobe. + * Copyright 2017-2026 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,10 @@ import org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties("com.adobe.testing.s3mock") @JvmRecord data class S3MockProperties( - // Property name for passing the HTTPS port to use. Defaults to - // {@value S3MockApplication#DEFAULT_HTTPS_PORT}. If set to - // {@value S3MockApplication#RANDOM_PORT}, a random port will be chosen. + /** + * Property name for passing the HTTPS port to use. Defaults to + * {@value S3MockApplication#DEFAULT_HTTPS_PORT}. If set to + * {@value S3MockApplication#RANDOM_PORT}, a random port will be chosen. + */ val httpPort: Int ) diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ChecksumModeHeaderConverter.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ChecksumModeHeaderConverter.kt index 35c8a3d32..81101e8f2 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ChecksumModeHeaderConverter.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ChecksumModeHeaderConverter.kt @@ -19,7 +19,7 @@ import com.adobe.testing.s3mock.dto.ChecksumMode import org.springframework.core.convert.converter.Converter /** - * Converts values of the [AwsHttpHeaders.X_AMZ_CHECKSUM_MODE] which is sent by the Amazon + * Converts values of the [com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CHECKSUM_MODE] which is sent by the Amazon * client. * Example: x-amz-checksum-mode: ENABLED * [API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ControllerProperties.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ControllerProperties.kt index f8853a16b..f38848863 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ControllerProperties.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/controller/ControllerProperties.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2025 Adobe. + * Copyright 2017-2026 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ import org.springframework.boot.context.properties.bind.DefaultValue @ConfigurationProperties("com.adobe.testing.s3mock.controller") @JvmRecord -data class ControllerProperties( // Property name for passing the global context path to use. - // Defaults to "". - // For example if set to `s3-mock` all endpoints will be available at - // `http://host:port/s3-mock` instead of `http://host:port/` +data class ControllerProperties( + /** + * Property name for passing the global context path to use. + * Defaults to "". + * For example if set to `s3-mock` all endpoints will be available at `http://host:port/s3-mock` instead of `http://host:port/` + */ @param:DefaultValue("") val contextPath: String ) diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/controller/MultipartController.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/controller/MultipartController.kt index 64ca0e973..f6e60e2ce 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/controller/MultipartController.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/controller/MultipartController.kt @@ -23,6 +23,7 @@ import com.adobe.testing.s3mock.dto.CompleteMultipartUploadResult import com.adobe.testing.s3mock.dto.CopyPartResult import com.adobe.testing.s3mock.dto.CopySource import com.adobe.testing.s3mock.dto.InitiateMultipartUploadResult +import com.adobe.testing.s3mock.dto.Initiator import com.adobe.testing.s3mock.dto.ListMultipartUploadsResult import com.adobe.testing.s3mock.dto.ListPartsResult import com.adobe.testing.s3mock.dto.ObjectKey @@ -60,6 +61,7 @@ import com.adobe.testing.s3mock.util.HeaderUtil.checksumAlgorithmFromHeader import com.adobe.testing.s3mock.util.HeaderUtil.checksumAlgorithmFromSdk import com.adobe.testing.s3mock.util.HeaderUtil.checksumFrom import com.adobe.testing.s3mock.util.HeaderUtil.checksumHeaderFrom +import com.adobe.testing.s3mock.util.HeaderUtil.checksumTypeFrom import com.adobe.testing.s3mock.util.HeaderUtil.encryptionHeadersFrom import com.adobe.testing.s3mock.util.HeaderUtil.storeHeadersFrom import com.adobe.testing.s3mock.util.HeaderUtil.userMetadataFrom @@ -381,7 +383,7 @@ class MultipartController( contentType, storeHeadersFrom(httpHeaders), Owner.DEFAULT_OWNER, - Owner.DEFAULT_OWNER, + Initiator.DEFAULT_INITIATOR, userMetadataFrom(httpHeaders), encryptionHeaders, tags, @@ -433,7 +435,7 @@ class MultipartController( multipartService.verifyMultipartParts(bucketName, objectName, uploadId, upload.parts) } val s3ObjectMetadata = objectService.getObject(bucketName, key.key, null) - objectService.verifyObjectMatching(match, noneMatch, null, null, s3ObjectMetadata) + objectService.verifyObjectMatching(bucketName, key.key, match, noneMatch) val locationWithEncodedKey = request .requestURL .toString() @@ -449,6 +451,7 @@ class MultipartController( encryptionHeadersFrom(httpHeaders), locationWithEncodedKey, checksumFrom(httpHeaders), + checksumTypeFrom(httpHeaders), checksumAlgorithmFromHeader(httpHeaders) )!! } else { diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Bucket.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Bucket.kt index b4a9c1fc6..c9eb80a4d 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Bucket.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Bucket.kt @@ -31,7 +31,7 @@ data class Bucket( @param:JsonProperty("CreationDate", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") val creationDate: String?, @param:JsonProperty("Name", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") - val name: String, + val name: String?, @JsonIgnore val path: Path? ) { diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ChecksumType.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ChecksumType.kt index 5cd6c1136..900c7a100 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ChecksumType.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ChecksumType.kt @@ -33,10 +33,12 @@ enum class ChecksumType @JsonCreator constructor(private val value: String) { } companion object { - fun fromString(value: String): ChecksumType? { + fun fromString(value: String?): ChecksumType? { return when (value) { "composite" -> COMPOSITE + "COMPOSITE" -> COMPOSITE "full_object" -> FULL_OBJECT + "FULL_OBJECT" -> FULL_OBJECT else -> null } } diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Initiator.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Initiator.kt new file mode 100644 index 000000000..15680dfad --- /dev/null +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Initiator.kt @@ -0,0 +1,35 @@ +/* + * 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. + * 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 com.adobe.testing.s3mock.dto + +import com.adobe.testing.S3Verified +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * [API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/API_Initiator.html). + */ +@S3Verified(year = 2025) +data class Initiator( + @param:JsonProperty("DisplayName", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") + val displayName: String?, + @param:JsonProperty("ID", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") + val id: String? +) { + companion object { + val DEFAULT_INITIATOR: Initiator = + Initiator("s3-mock-file-store", "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") + } +} diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ListPartsResult.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ListPartsResult.kt index bf83b5b20..73a45fd42 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ListPartsResult.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/ListPartsResult.kt @@ -34,7 +34,7 @@ data class ListPartsResult( @param:JsonProperty("ChecksumType", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") val checksumType: ChecksumType?, @param:JsonProperty("Initiator", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") - val initiator: Owner?, + val initiator: Initiator?, @param:JsonProperty("IsTruncated", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") val isTruncated: Boolean, @param:JsonProperty("Key", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/MultipartUpload.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/MultipartUpload.kt index 0539ce6e8..657bac141 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/MultipartUpload.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/MultipartUpload.kt @@ -34,7 +34,7 @@ data class MultipartUpload( @param:JsonProperty("Initiated", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") val initiated: Date?, @param:JsonProperty("Initiator", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") - val initiator: Owner?, + val initiator: Initiator?, @param:JsonProperty("Key", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") val key: String, @param:JsonProperty("Owner", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Owner.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Owner.kt index f561d4479..536fb7356 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Owner.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/dto/Owner.kt @@ -24,9 +24,6 @@ import com.fasterxml.jackson.annotation.JsonProperty */ @S3Verified(year = 2025) data class Owner( - @Deprecated("AWS deprecated this field in 2025-05") - @param:JsonProperty("DisplayName", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") - val displayName: String? = null, @param:JsonProperty("ID", namespace = "http://s3.amazonaws.com/doc/2006-03-01/") val id: String? ) { @@ -34,9 +31,7 @@ data class Owner( /** * Default owner in S3Mock until support for ownership is implemented. */ - val DEFAULT_OWNER: Owner = - Owner("s3-mock-file-store", "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") - val DEFAULT_OWNER_BUCKET: Owner = - Owner("s3-mock-file-store-bucket", "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2df") + val DEFAULT_OWNER: Owner = Owner("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") + val DEFAULT_OWNER_BUCKET: Owner = Owner("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2df") } } diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/service/BucketService.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/service/BucketService.kt index 64557d176..25b09aa7c 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/service/BucketService.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/service/BucketService.kt @@ -43,6 +43,7 @@ import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_LOCATION_TYPE import software.amazon.awssdk.utils.http.SdkHttpUtils.urlEncodeIgnoreSlashes import java.util.UUID import java.util.concurrent.ConcurrentHashMap +import kotlin.let open class BucketService( private val bucketStore: BucketStore, @@ -92,7 +93,9 @@ open class BucketService( if (buckets.size > maxBuckets) { nextContinuationToken = UUID.randomUUID().toString() buckets = buckets.subList(0, maxBuckets) - listBucketsPagingStateCache[nextContinuationToken] = buckets[maxBuckets - 1].name + buckets[maxBuckets - 1].name?.let { + listBucketsPagingStateCache[nextContinuationToken] = it + } } return ListAllMyBucketsResult(Owner.DEFAULT_OWNER, Buckets(buckets), prefix, nextContinuationToken) diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/service/MultipartService.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/service/MultipartService.kt index 80bb22f57..8d950d0a7 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/service/MultipartService.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/service/MultipartService.kt @@ -22,6 +22,7 @@ import com.adobe.testing.s3mock.dto.CompleteMultipartUploadResult import com.adobe.testing.s3mock.dto.CompletedPart import com.adobe.testing.s3mock.dto.CopyPartResult import com.adobe.testing.s3mock.dto.InitiateMultipartUploadResult +import com.adobe.testing.s3mock.dto.Initiator import com.adobe.testing.s3mock.dto.ListMultipartUploadsResult import com.adobe.testing.s3mock.dto.ListPartsResult import com.adobe.testing.s3mock.dto.MultipartUpload @@ -161,6 +162,7 @@ open class MultipartService(private val bucketStore: BucketStore, private val mu encryptionHeaders: Map, location: String, checksum: String?, + checksumType: ChecksumType?, checksumAlgorithm: ChecksumAlgorithm? ): CompleteMultipartUploadResult? { val bucketMetadata = bucketStore.getBucketMetadata(bucketName) @@ -177,6 +179,7 @@ open class MultipartService(private val bucketStore: BucketStore, private val mu multipartUploadInfo, location, checksum, + checksumType, checksumAlgorithm ) } @@ -187,7 +190,7 @@ open class MultipartService(private val bucketStore: BucketStore, private val mu contentType: String?, storeHeaders: Map, owner: Owner, - initiator: Owner, + initiator: Initiator, userMetadata: Map, encryptionHeaders: Map, tags: List?, diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/service/ServiceBase.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/service/ServiceBase.kt index d2bfd4842..2517b404d 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/service/ServiceBase.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/service/ServiceBase.kt @@ -93,15 +93,12 @@ abstract class ServiceBase { fun filterBy( contents: List, - selector: (T) -> String, + selector: (T) -> String?, compareTo: String? - ): List { - return if (!compareTo.isNullOrEmpty()) { - contents.filter { selector(it) > compareTo } - } else { - contents - } - } + ): List = + compareTo?.let { threshold -> + contents.filter { selector(it)?.let { candidate -> candidate > threshold } == true } + } ?: contents fun filterBy( contents: List, @@ -117,11 +114,11 @@ abstract class ServiceBase { fun filterBy( contents: List, - selector: (T) -> String, + selector: (T) -> String?, prefixes: List? ): List { return if (!prefixes.isNullOrEmpty()) { - contents.filter { content -> prefixes.none { prefix -> selector(content).startsWith(prefix) } } + contents.filter { content -> prefixes.none { prefix -> selector(content)?.startsWith(prefix) ?: false } } } else { contents } diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/store/MultipartStore.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/store/MultipartStore.kt index a359a9599..fba238fdd 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/store/MultipartStore.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/store/MultipartStore.kt @@ -21,6 +21,7 @@ import com.adobe.testing.s3mock.dto.ChecksumAlgorithm import com.adobe.testing.s3mock.dto.ChecksumType import com.adobe.testing.s3mock.dto.CompleteMultipartUploadResult import com.adobe.testing.s3mock.dto.CompletedPart +import com.adobe.testing.s3mock.dto.Initiator import com.adobe.testing.s3mock.dto.MultipartUpload import com.adobe.testing.s3mock.dto.Owner import com.adobe.testing.s3mock.dto.Part @@ -64,7 +65,7 @@ open class MultipartStore(private val objectStore: ObjectStore, private val obje contentType: String?, storeHeaders: Map, owner: Owner, - initiator: Owner, + initiator: Initiator, userMetadata: Map, encryptionHeaders: Map, tags: List?, @@ -189,6 +190,7 @@ open class MultipartStore(private val objectStore: ObjectStore, private val obje uploadInfo: MultipartUploadInfo?, location: String, checksum: String?, + checksumType: ChecksumType?, checksumAlgorithm: ChecksumAlgorithm? ): CompleteMultipartUploadResult { requireNotNull(uploadInfo) { "Unknown upload $uploadId" } @@ -201,42 +203,42 @@ open class MultipartStore(private val objectStore: ObjectStore, private val obje toInputStream(partsPaths).use { input -> tempFile.outputStream().use { os -> input.transferTo(os) - val checksumFor = validateChecksums(uploadInfo, parts, partsPaths, checksum, checksumAlgorithm) - val etag = DigestUtil.hexDigestMultipart(partsPaths) - val s3ObjectMetadata = objectStore.storeS3ObjectMetadata( - bucket, - id, - key, - uploadInfo.contentType, - uploadInfo.storeHeaders, - tempFile, - uploadInfo.userMetadata, - encryptionHeaders, - etag, - uploadInfo.tags, - uploadInfo.checksumAlgorithm, - checksumFor, - uploadInfo.upload.owner, - uploadInfo.storageClass, - ChecksumType.COMPOSITE - ) - // delete parts and update MultipartInfo - partsPaths.forEach { runCatching { it.toFile().deleteRecursively() } } - val completedUploadInfo = uploadInfo.complete() - writeMetafile(bucket, completedUploadInfo) - return CompleteMultipartUploadResult.from( - location, - completedUploadInfo.bucket, - key, - etag, - completedUploadInfo, - checksumFor, - s3ObjectMetadata.checksumType, - checksumAlgorithm, - s3ObjectMetadata.versionId - ) } } + val checksumFor = validateChecksums(uploadInfo, tempFile, parts, partsPaths, checksum, checksumType, checksumAlgorithm) + val etag = DigestUtil.hexDigestMultipart(partsPaths) + val s3ObjectMetadata = objectStore.storeS3ObjectMetadata( + bucket, + id, + key, + uploadInfo.contentType, + uploadInfo.storeHeaders, + tempFile, + uploadInfo.userMetadata, + encryptionHeaders, + etag, + uploadInfo.tags, + uploadInfo.checksumAlgorithm, + checksumFor, + uploadInfo.upload.owner, + uploadInfo.storageClass, + checksumType + ) + // delete parts and update MultipartInfo + partsPaths.forEach { runCatching { it.toFile().deleteRecursively() } } + val completedUploadInfo = uploadInfo.complete() + writeMetafile(bucket, completedUploadInfo) + return CompleteMultipartUploadResult.from( + location, + completedUploadInfo.bucket, + key, + etag, + completedUploadInfo, + checksumFor, + s3ObjectMetadata.checksumType, + checksumAlgorithm, + s3ObjectMetadata.versionId + ) } catch (e: IOException) { throw IllegalStateException("Error finishing multipart upload bucket=$bucket, key=$key, id=$id, uploadId=$uploadId", e) } finally { @@ -421,14 +423,24 @@ open class MultipartStore(private val objectStore: ObjectStore, private val obje private fun validateChecksums( uploadInfo: MultipartUploadInfo, + tempFile: Path, completedParts: List, partsPaths: List, checksum: String?, + checksumType: ChecksumType?, checksumAlgorithm: ChecksumAlgorithm? ): String? { val checksumToValidate = checksum ?: uploadInfo.checksum val checksumAlgorithmToValidate = checksumAlgorithm ?: uploadInfo.checksumAlgorithm - val checksumFor = checksumFor(partsPaths, uploadInfo) + if(checksumType != null && uploadInfo.checksumType != null && checksumType != uploadInfo.checksumType) { + throw S3Exception.completeRequestWrongChecksumMode(uploadInfo.checksumType.name) + } + val checksumFor = if (uploadInfo.checksumType == ChecksumType.COMPOSITE) { + checksumFor(partsPaths, uploadInfo) + } else { + checksumFor(tempFile, uploadInfo) + } + if (checksumAlgorithmToValidate != null) { completedParts.forEach { part -> if (part.checksum(checksumAlgorithmToValidate) == null) { @@ -451,6 +463,11 @@ open class MultipartStore(private val objectStore: ObjectStore, private val obje DigestUtil.checksumMultipart(paths, algo.toChecksumAlgorithm()) } + private fun checksumFor(path: Path, uploadInfo: MultipartUploadInfo): String? = + uploadInfo.checksumAlgorithm?.let { algo -> + DigestUtil.checksumFor(path, algo.toChecksumAlgorithm()) + } + companion object { private val LOG: Logger = LoggerFactory.getLogger(MultipartStore::class.java) private const val PART_SUFFIX = ".part" diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/store/ObjectStore.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/store/ObjectStore.kt index e9670a57e..f465313a1 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/store/ObjectStore.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/store/ObjectStore.kt @@ -121,7 +121,7 @@ open class ObjectStore( private fun privateCannedAcl(owner: Owner): AccessControlPolicy { val grant = Grant( - CanonicalUser(owner.displayName, owner.id), + CanonicalUser(null, owner.id), Grant.Permission.FULL_CONTROL ) return AccessControlPolicy(listOf(grant), owner) diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadata.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadata.kt index 7b0c69121..7ee39f74f 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadata.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadata.kt @@ -53,7 +53,7 @@ data class S3ObjectMetadata( val policy: AccessControlPolicy?, val versionId: String?, val deleteMarker: Boolean = false, - val checksumType: ChecksumType? = ChecksumType.FULL_OBJECT + val checksumType: ChecksumType? ) { companion object { fun deleteMarker(metadata: S3ObjectMetadata, versionId: String?): S3ObjectMetadata = diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/store/StoreProperties.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/store/StoreProperties.kt index 4e1b2f9fa..253e0815c 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/store/StoreProperties.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/store/StoreProperties.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2025 Adobe. + * Copyright 2017-2026 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,15 +23,26 @@ import software.amazon.awssdk.regions.Region @JvmRecord @ConfigurationProperties("com.adobe.testing.s3mock.store") data class StoreProperties( -// True if files should be retained when S3Mock exits gracefully. - // False to let S3Mock delete all files when S3Mock exits gracefully. + /** + * True if files should be retained when S3Mock exits gracefully. + * False to let S3Mock delete all files when S3Mock exits gracefully. + */ @param:DefaultValue("false") val retainFilesOnExit: Boolean, - // The root directory to use. If omitted a default temp-dir will be used. + /** + * The root directory to use. If omitted a default temp-dir will be used. + */ @param:DefaultValue("") val root: String, + /** + * A set of KMS keys that S3Mock should treat as valid. + */ @param:DefaultValue("") val validKmsKeys: Set, - // A comma separated list of buckets that are to be created at startup. + /** + * A comma separated list of buckets that are to be created at startup. + */ @param:DefaultValue("") val initialBuckets: List, - // Region is S3Mock is supposed to mock. - // Must be an official AWS region string like "us-east-1" + /** + * Region is S3Mock is supposed to mock. + * Must be an official AWS region string like "us-east-1" + */ @param:DefaultValue("us-east-1") val region: Region ) diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/util/CannedAclUtil.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/util/CannedAclUtil.kt index b1acd7366..49d99d2bc 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/util/CannedAclUtil.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/util/CannedAclUtil.kt @@ -42,7 +42,7 @@ object CannedAclUtil { } private val defaultOwner = Owner.DEFAULT_OWNER - private val defaultOwnerUser = CanonicalUser(defaultOwner.displayName, defaultOwner.id) + private val defaultOwnerUser = CanonicalUser("s3-mock-file-store", defaultOwner.id) private fun policyWithOwner(vararg additionalGrants: Grant): AccessControlPolicy = AccessControlPolicy( @@ -54,7 +54,7 @@ object CannedAclUtil { policyWithOwner( Grant( CanonicalUser( - Owner.DEFAULT_OWNER_BUCKET.displayName, + "s3-mock-file-store", Owner.DEFAULT_OWNER_BUCKET.id ), Grant.Permission.READ @@ -65,7 +65,7 @@ object CannedAclUtil { policyWithOwner( Grant( CanonicalUser( - Owner.DEFAULT_OWNER_BUCKET.displayName, + "s3-mock-file-store", Owner.DEFAULT_OWNER_BUCKET.id ), Grant.Permission.READ diff --git a/server/src/main/kotlin/com/adobe/testing/s3mock/util/HeaderUtil.kt b/server/src/main/kotlin/com/adobe/testing/s3mock/util/HeaderUtil.kt index 2f206e2d8..c7e0ef72c 100644 --- a/server/src/main/kotlin/com/adobe/testing/s3mock/util/HeaderUtil.kt +++ b/server/src/main/kotlin/com/adobe/testing/s3mock/util/HeaderUtil.kt @@ -17,6 +17,7 @@ package com.adobe.testing.s3mock.util import com.adobe.testing.s3mock.dto.ChecksumAlgorithm +import com.adobe.testing.s3mock.dto.ChecksumType import com.adobe.testing.s3mock.dto.CopySource import com.adobe.testing.s3mock.dto.StorageClass import com.adobe.testing.s3mock.store.S3ObjectMetadata @@ -27,6 +28,7 @@ import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CHECKSUM_CRC32C import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CHECKSUM_CRC64NVME import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CHECKSUM_SHA1 import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CHECKSUM_SHA256 +import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CHECKSUM_TYPE import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CONTENT_SHA256 import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_COPY_SOURCE_VERSION_ID import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_SDK_CHECKSUM_ALGORITHM @@ -231,6 +233,13 @@ object HeaderUtil { else null } + @JvmStatic + fun checksumTypeFrom(headers: HttpHeaders): ChecksumType? { + return if (headers.containsHeader(X_AMZ_CHECKSUM_TYPE)) + ChecksumType.fromString(headers.getFirst(X_AMZ_CHECKSUM_TYPE)) + else null + } + @JvmStatic fun checksumFrom(headers: HttpHeaders): String? { return when { diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties index 9a429e10e..3d88ecb37 100644 --- a/server/src/main/resources/application.properties +++ b/server/src/main/resources/application.properties @@ -25,7 +25,12 @@ spring.jmx.enabled=false com.adobe.testing.s3mock.store.region=us-east-1 # allow S3Mock to consume larger payloads -server.tomcat.max-swallow-size=10MB +com.adobe.testing.s3mock.max-payload-size=10MB +server.tomcat.max-part-count=-1 +server.tomcat.max-swallow-size=${com.adobe.testing.s3mock.max-payload-size} +server.tomcat.max-http-form-post-size=${com.adobe.testing.s3mock.max-payload-size} +spring.servlet.multipart.max-file-size=${com.adobe.testing.s3mock.max-payload-size} +spring.servlet.multipart.max-request-size=${com.adobe.testing.s3mock.max-payload-size} management.endpoints.web.discovery.enabled=false diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BaseControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BaseControllerTest.kt index d15308df8..65149b15f 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BaseControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BaseControllerTest.kt @@ -140,7 +140,7 @@ internal abstract class BaseControllerTest { put(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, encryptionKey) } } - val TEST_OWNER = Owner("s3-mock-file-store", "123") + val TEST_OWNER = Owner("123") val TEST_BUCKETMETADATA = bucketMetadata() const val UPLOAD_FILE_NAME = "src/test/resources/sampleFile.txt" diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BucketControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BucketControllerTest.kt index ca92c23b4..6e7e23da4 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BucketControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/BucketControllerTest.kt @@ -80,7 +80,10 @@ import java.nio.file.Path import java.time.Instant @MockitoBean(types = [KmsKeyStore::class, ObjectService::class, MultipartService::class, ObjectController::class, MultipartController::class]) -@WebMvcTest(properties = ["com.adobe.testing.s3mock.store.region=us-east-1"]) +@WebMvcTest( + controllers = [BucketController::class], + properties = ["com.adobe.testing.s3mock.store.region=us-east-1"] +) internal class BucketControllerTest : BaseControllerTest() { @MockitoBean private lateinit var bucketService: BucketService diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ContextPathObjectStoreControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ContextPathObjectStoreControllerTest.kt index 64cb537ac..58d101886 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ContextPathObjectStoreControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ContextPathObjectStoreControllerTest.kt @@ -37,7 +37,10 @@ import java.nio.file.Paths import java.time.Instant @MockitoBean(types = [KmsKeyStore::class, ObjectService::class, MultipartService::class, MultipartStore::class]) -@WebMvcTest(properties = ["com.adobe.testing.s3mock.controller.contextPath=s3-mock"]) +@WebMvcTest( + controllers = [ObjectController::class], + properties = ["com.adobe.testing.s3mock.controller.contextPath=s3-mock"] +) internal class ContextPathObjectStoreControllerTest : BaseControllerTest() { @MockitoBean private lateinit var bucketService: BucketService diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/FaviconControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/FaviconControllerTest.kt index b186b7169..5f457dfe7 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/FaviconControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/FaviconControllerTest.kt @@ -26,7 +26,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @MockitoBean(types = [KmsKeyStore::class, ObjectController::class, BucketController::class, MultipartController::class]) -@WebMvcTest +@WebMvcTest(controllers = [FaviconController::class]) internal class FaviconControllerTest : BaseControllerTest() { @Autowired private lateinit var mockMvc: MockMvc diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/KmsValidationFilterTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/KmsValidationFilterTest.kt new file mode 100644 index 000000000..17327681d --- /dev/null +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/KmsValidationFilterTest.kt @@ -0,0 +1,109 @@ +/* + * 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. + * 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 com.adobe.testing.s3mock.controller + +import com.adobe.testing.s3mock.store.KmsKeyStore +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@MockitoBean(types = [KmsKeyStore::class]) +@WebMvcTest( + controllers = [KmsValidationFilterTest.DummyPassController::class], + properties = ["com.adobe.testing.s3mock.store.region=us-east-1"], + useDefaultFilters = false +) +@Import(KmsValidationFilterTest.Config::class) +internal class KmsValidationFilterTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var kmsKeyStore: KmsKeyStore + + + @Test + fun `denies request with invalid kms key id`() { + val badKey = "bad-key-id" + whenever(kmsKeyStore.validateKeyId(badKey)).thenReturn(false) + + mockMvc.perform( + get("/internal-test/pass") + .param("dummy", "true") + .header("X-Dummy", "1") + .header("x-amz-server-side-encryption", "aws:kms") + .header("x-amz-server-side-encryption-aws-kms-key-id", badKey) + ) + .andExpect(status().isBadRequest) + .andExpect(content().contentType(MediaType.APPLICATION_XML)) + .andExpect(content().string(org.hamcrest.Matchers.containsString("Invalid keyId '$badKey'"))) + } + + @Test + fun `allows request with valid kms key id`() { + val goodKey = "good-key-id" + whenever(kmsKeyStore.validateKeyId(goodKey)).thenReturn(true) + + mockMvc.perform( + get("/internal-test/pass") + .param("dummy", "true") + .header("X-Dummy", "1") + .header("x-amz-server-side-encryption", "aws:kms") + .header("x-amz-server-side-encryption-aws-kms-key-id", goodKey) + ) + .andExpect { assertThat(it.response.status).isNotEqualTo(400) } + } + + @Test + fun `allows request when kms headers are missing`() { + mockMvc.perform( + get("/internal-test/pass") + .param("dummy", "true") + .header("X-Dummy", "1") + ).andExpect { assertThat(it.response.status).isNotEqualTo(400) } + } + + @TestConfiguration + internal class Config { + @Bean + fun kmsValidationFilter( + kmsKeyStore: KmsKeyStore + ) = KmsValidationFilter(kmsKeyStore, ControllerConfiguration().messageConverter()) + } + + @RestController + @RequestMapping("/internal-test", params = ["dummy=true"]) + internal class DummyPassController { + @GetMapping("/pass", headers = ["X-Dummy=1"]) + fun pass(): ResponseEntity = ResponseEntity.ok("ok") + } +} diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/MultipartControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/MultipartControllerTest.kt index 0b41c1395..eb64d646b 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/MultipartControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/MultipartControllerTest.kt @@ -23,6 +23,7 @@ import com.adobe.testing.s3mock.dto.CompleteMultipartUploadResult import com.adobe.testing.s3mock.dto.CompletedPart import com.adobe.testing.s3mock.dto.CopyPartResult import com.adobe.testing.s3mock.dto.InitiateMultipartUploadResult +import com.adobe.testing.s3mock.dto.Initiator import com.adobe.testing.s3mock.dto.ListMultipartUploadsResult import com.adobe.testing.s3mock.dto.ListPartsResult import com.adobe.testing.s3mock.dto.MultipartUpload @@ -63,7 +64,7 @@ import java.util.Date import java.util.UUID @MockitoBean(types = [KmsKeyStore::class, ObjectController::class, BucketController::class]) -@WebMvcTest +@WebMvcTest(controllers = [MultipartController::class]) internal class MultipartControllerTest : BaseControllerTest() { @MockitoBean private lateinit var bucketService: BucketService @@ -244,7 +245,7 @@ internal class MultipartControllerTest : BaseControllerTest() { whenever(objectService.getObject(TEST_BUCKET_NAME, key, null)).thenReturn(s3meta) // create result with encryption headers to be echoed - val mpUpload = MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) + val mpUpload = MultipartUpload(null, null, Date(), Initiator.DEFAULT_INITIATOR, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) val info = MultipartUploadInfo( mpUpload, "application/octet-stream", @@ -279,6 +280,7 @@ internal class MultipartControllerTest : BaseControllerTest() { anyOrNull(), any(), anyOrNull(), + anyOrNull(), anyOrNull() ) ).thenReturn(result) @@ -322,7 +324,7 @@ internal class MultipartControllerTest : BaseControllerTest() { val s3meta = s3ObjectMetadata(key, UUID.randomUUID().toString()) whenever(objectService.getObject(TEST_BUCKET_NAME, key, null)).thenReturn(s3meta) - val mpUpload = MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) + val mpUpload = MultipartUpload(null, null, Date(), Initiator.DEFAULT_INITIATOR, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) val info = MultipartUploadInfo( mpUpload, "application/octet-stream", @@ -357,6 +359,7 @@ internal class MultipartControllerTest : BaseControllerTest() { anyOrNull(), any(), anyOrNull(), + anyOrNull(), anyOrNull() ) ).thenReturn(result) @@ -395,7 +398,7 @@ internal class MultipartControllerTest : BaseControllerTest() { val s3meta = s3ObjectMetadata(key, UUID.randomUUID().toString()) whenever(objectService.getObject(TEST_BUCKET_NAME, key, null)).thenReturn(s3meta) - val mpUpload = MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) + val mpUpload = MultipartUpload(null, null, Date(), Initiator.DEFAULT_INITIATOR, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) val info = MultipartUploadInfo( mpUpload, "application/octet-stream", @@ -430,6 +433,7 @@ internal class MultipartControllerTest : BaseControllerTest() { anyOrNull(), any(), anyOrNull(), + anyOrNull(), anyOrNull() ) ).thenReturn(result) @@ -472,11 +476,10 @@ internal class MultipartControllerTest : BaseControllerTest() { doThrow(S3Exception.PRECONDITION_FAILED) .whenever(objectService) .verifyObjectMatching( + eq(TEST_BUCKET_NAME), + eq(key), anyOrNull(), anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(s3meta) ) val uri = UriComponentsBuilder @@ -571,7 +574,7 @@ internal class MultipartControllerTest : BaseControllerTest() { null, null, Date(), - Owner.DEFAULT_OWNER, + Initiator.DEFAULT_INITIATOR, "my/key.txt", Owner.DEFAULT_OWNER, StorageClass.STANDARD, @@ -632,7 +635,7 @@ internal class MultipartControllerTest : BaseControllerTest() { val uploadIdMarker = "u-marker" val uploads = listOf( - MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, "pre/a.txt", Owner.DEFAULT_OWNER, StorageClass.STANDARD, "u-1") + MultipartUpload(null, null, Date(), Initiator.DEFAULT_INITIATOR, "pre/a.txt", Owner.DEFAULT_OWNER, StorageClass.STANDARD, "u-1") ) val result = ListMultipartUploadsResult( TEST_BUCKET_NAME, @@ -687,7 +690,7 @@ internal class MultipartControllerTest : BaseControllerTest() { whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploads = listOf( - MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, "k1", Owner.DEFAULT_OWNER, StorageClass.STANDARD, "u-1") + MultipartUpload(null, null, Date(), Initiator.DEFAULT_INITIATOR, "k1", Owner.DEFAULT_OWNER, StorageClass.STANDARD, "u-1") ) val result = ListMultipartUploadsResult( @@ -828,7 +831,7 @@ internal class MultipartControllerTest : BaseControllerTest() { TEST_BUCKET_NAME, null, null, - Owner.DEFAULT_OWNER, + Initiator.DEFAULT_INITIATOR, false, "my/key.txt", 1000, @@ -877,7 +880,7 @@ internal class MultipartControllerTest : BaseControllerTest() { TEST_BUCKET_NAME, null, null, - Owner.DEFAULT_OWNER, + Initiator.DEFAULT_INITIATOR, false, "my/key.txt", maxParts, @@ -930,7 +933,7 @@ internal class MultipartControllerTest : BaseControllerTest() { TEST_BUCKET_NAME, null, null, - Owner.DEFAULT_OWNER, + Initiator.DEFAULT_INITIATOR, true, "my/key.txt", maxParts, @@ -1460,7 +1463,7 @@ internal class MultipartControllerTest : BaseControllerTest() { argThat { this.startsWith("application/octet-stream") }, anyOrNull(), eq(Owner.DEFAULT_OWNER), - eq(Owner.DEFAULT_OWNER), + eq(Initiator.DEFAULT_INITIATOR), anyOrNull>(), anyOrNull>(), anyOrNull>(), @@ -1528,7 +1531,7 @@ internal class MultipartControllerTest : BaseControllerTest() { anyOrNull(), anyOrNull(), eq(Owner.DEFAULT_OWNER), - eq(Owner.DEFAULT_OWNER), + eq(Initiator.DEFAULT_INITIATOR), anyOrNull>(), eq(mapOf("x-amz-server-side-encryption" to "AES256")), anyOrNull>(), @@ -1571,7 +1574,7 @@ internal class MultipartControllerTest : BaseControllerTest() { anyOrNull(), anyOrNull(), eq(Owner.DEFAULT_OWNER), - eq(Owner.DEFAULT_OWNER), + eq(Initiator.DEFAULT_INITIATOR), anyOrNull>(), anyOrNull>(), anyOrNull>(), @@ -1613,7 +1616,7 @@ internal class MultipartControllerTest : BaseControllerTest() { eq(null), anyOrNull(), eq(Owner.DEFAULT_OWNER), - eq(Owner.DEFAULT_OWNER), + eq(Initiator.DEFAULT_INITIATOR), anyOrNull>(), anyOrNull>(), anyOrNull>(), diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ObjectControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ObjectControllerTest.kt index 0d771b2d3..6a860f5d1 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ObjectControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/controller/ObjectControllerTest.kt @@ -79,7 +79,10 @@ import java.time.Instant import java.util.UUID @MockitoBean(types = [KmsKeyStore::class, MultipartService::class, BucketController::class, MultipartController::class]) -@WebMvcTest(properties = ["com.adobe.testing.s3mock.store.region=us-east-1"]) +@WebMvcTest( + controllers = [ObjectController::class], + properties = ["com.adobe.testing.s3mock.store.region=us-east-1"] +) internal class ObjectControllerTest : BaseControllerTest() { @MockitoBean private lateinit var objectService: ObjectService @@ -409,11 +412,8 @@ internal class ObjectControllerTest : BaseControllerTest() { givenBucket() val key = "name" - val owner = Owner( - "mtd@amazon.com", - "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" - ) - val grantee = CanonicalUser(owner.displayName, owner.id) + val owner = Owner("75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a") + val grantee = CanonicalUser(null, owner.id) val policy = AccessControlPolicy( listOf(Grant(grantee, Grant.Permission.FULL_CONTROL)), owner @@ -443,11 +443,8 @@ internal class ObjectControllerTest : BaseControllerTest() { givenBucket() val key = "name" - val owner = Owner( - "mtd@amazon.com", - "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" - ) - val grantee = CanonicalUser(owner.displayName, owner.id) + val owner = Owner("75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a") + val grantee = CanonicalUser(null, owner.id) val policy = AccessControlPolicy( listOf(Grant(grantee, Grant.Permission.FULL_CONTROL)), owner diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt index 0211d81cb..fd67fc472 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt @@ -30,10 +30,7 @@ internal class AccessControlPolicyTest { val owner = iut.owner assertThat(owner).isNotNull() - assertThat(owner?.id).isEqualTo( - "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" - ) - assertThat(owner?.displayName).isEqualTo("mtd@amazon.com") + assertThat(owner!!.id).isEqualTo("75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a") assertThat(iut.accessControlList).hasSize(3) iut.accessControlList?.get(0).also { @@ -65,11 +62,8 @@ internal class AccessControlPolicyTest { @Test fun testSerialization(testInfo: TestInfo) { - val owner = Owner( - "mtd@amazon.com", - "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" - ) - val grantee = CanonicalUser(owner.displayName, owner.id) + val owner = Owner("75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a") + val grantee = CanonicalUser("mtd@amazon.com", owner.id) val group = Group(URI.create("http://acs.amazonaws.com/groups/s3/LogDelivery")) val customer = AmazonCustomerByEmail("xyz@amazon.com") diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResultTest.kt index 893de6b1c..7db778ad9 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResultTest.kt @@ -35,9 +35,9 @@ internal class CompleteMultipartUploadResultTest { ChecksumAlgorithm.SHA256, ChecksumType.COMPOSITE, Date(1514477008120L), - Owner("displayName100", (100L).toString()), + Initiator("displayName100", (100L).toString()), "key", - Owner("displayName10", (10L).toString()), + Owner((10L).toString()), StorageClass.STANDARD, "uploadId", ), diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt index ed40468df..fd31afb33 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt @@ -26,7 +26,7 @@ internal class ListAllMyBucketsResultTest { fun testSerialization(testInfo: TestInfo) { val iut = ListAllMyBucketsResult( - Owner("displayName", 10L.toString()), + Owner(10L.toString()), createBuckets(), "some-prefix", "50d8e003-0451-48fd-9c49-8208b151649c" diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt index 05dfca094..fe89fda96 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt @@ -48,7 +48,7 @@ internal class ListBucketResultTest { "\"fba9dede5f27731c9771645a39863328\"", "key$it", "2009-10-12T17:50:30.000Z", - Owner("displayName", (10L + it).toString()), + Owner((10L + it).toString()), null, "434234", StorageClass.STANDARD diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt index 104e7fbc4..53384ab97 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt @@ -53,7 +53,7 @@ internal class ListBucketResultV2Test { "\"fba9dede5f27731c9771645a39863328\"", "key$it", "2009-10-12T17:50:30.000Z", - Owner("displayName", (10L + it).toString()), + Owner((10L + it).toString()), null, "434234", StorageClass.STANDARD diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt index b8b19c48b..e12d5b2fb 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt @@ -49,9 +49,9 @@ internal class ListMultipartUploadsResultTest { ChecksumAlgorithm.SHA256, ChecksumType.COMPOSITE, Date(1514477008120L), - Owner("displayName100$it", (100L + it).toString()), + Initiator("displayName100$it", (100L + it).toString()), "key$it", - Owner("displayName10$it", (10L + it).toString()), + Owner((10L + it).toString()), StorageClass.STANDARD, "uploadId$it", ) diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt index e95a73e74..ab83e9912 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt @@ -29,12 +29,12 @@ internal class ListPartsResultTest { "bucketName", ChecksumAlgorithm.CRC32, ChecksumType.COMPOSITE, - Owner("id", "displayName"), + Initiator("id", "displayName"), false, "fileName", 1000, 100, - Owner("id", "displayName"), + Owner("displayName"), createParts(), 5, StorageClass.STANDARD, diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt index 3130d3b05..d4a09c28f 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/service/ServiceTestBase.kt @@ -148,7 +148,7 @@ internal abstract class ServiceTestBase { val lastModified = "lastModified" val etag = "etag" val size = "size" - val owner = Owner("name", 0L.toString()) + val owner = Owner(0L.toString()) return S3Object( ChecksumAlgorithm.SHA256, ChecksumType.FULL_OBJECT, @@ -166,7 +166,7 @@ internal abstract class ServiceTestBase { val lastModified = "lastModified" val etag = "etag" val size = "size" - val owner = Owner("name", 0L.toString()) + val owner = Owner(0L.toString()) return S3ObjectMetadata( id, key, diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/MultipartStoreTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/MultipartStoreTest.kt index 8c4fa99fc..adb967699 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/MultipartStoreTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/MultipartStoreTest.kt @@ -83,7 +83,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -123,7 +123,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -172,7 +172,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -211,6 +211,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) val md5 = MessageDigest.getInstance("MD5") @@ -251,7 +252,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -288,6 +289,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) @@ -318,7 +320,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, userMetadata, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -355,6 +357,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) @@ -389,7 +392,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, userMetadata, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -445,6 +448,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", checksum, + ChecksumType.COMPOSITE, ChecksumAlgorithm.CRC32, ) } @@ -473,7 +477,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -504,6 +508,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", wrongOverallChecksum, + ChecksumType.COMPOSITE, ChecksumAlgorithm.CRC32, ) }.isInstanceOf(S3Exception::class.java) @@ -532,7 +537,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, userMetadata, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -570,6 +575,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", checksum, + ChecksumType.COMPOSITE, ChecksumAlgorithm.CRC32, ) }.isInstanceOf(S3Exception::class.java) @@ -609,7 +615,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -673,7 +679,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -704,6 +710,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) @@ -728,7 +735,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -757,6 +764,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) @@ -782,7 +790,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -802,7 +810,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -842,6 +850,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo1, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) multipartStore.completeMultipartUpload( @@ -854,6 +863,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo2, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) @@ -876,7 +886,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -953,7 +963,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -1025,7 +1035,7 @@ internal class MultipartStoreTest : StoreTestBase() { DEFAULT_CONTENT_TYPE, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -1096,7 +1106,7 @@ internal class MultipartStoreTest : StoreTestBase() { TEXT_PLAIN, storeHeaders(), TEST_OWNER, - TEST_OWNER, + TEST_INITIATOR, NO_USER_METADATA, NO_ENCRYPTION_HEADERS, NO_TAGS, @@ -1125,6 +1135,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartUploadInfo, "location", NO_CHECKSUM, + NO_CHECKSUMTYPE, NO_CHECKSUM_ALGORITHM, ) val s = objectStore.getS3ObjectMetadata(bucket, id, null)!!.dataPath diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/ObjectStoreTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/ObjectStoreTest.kt index 46a9dec25..f4871e76f 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/ObjectStoreTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/ObjectStoreTest.kt @@ -333,11 +333,8 @@ internal class ObjectStoreTest : StoreTestBase() { @Test fun testStoreAndRetrieveAcl() { - val owner = Owner( - "mtd@amazon.com", - "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" - ) - val grantee = CanonicalUser(owner.displayName, owner.id) + val owner = Owner("75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a") + val grantee = CanonicalUser(null, owner.id) val policy = AccessControlPolicy( listOf(Grant(grantee, Grant.Permission.FULL_CONTROL)), owner diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadataTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadataTest.kt index 286fa7f1f..0aaceb969 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadataTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/S3ObjectMetadataTest.kt @@ -57,7 +57,6 @@ class S3ObjectMetadataTest { assertThat(iut.legalHold).isEqualTo(LegalHold(LegalHold.Status.ON)) assertThat(iut.retention).isEqualTo(Retention(Mode.GOVERNANCE, Instant.parse("2025-09-12T22:50:49.038Z"))) assertThat(iut.owner).isNotNull() - assertThat(iut.owner.displayName).isEqualTo("s3-mock-file-store") assertThat(iut.owner.id).isEqualTo("79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be") assertThat(iut.storeHeaders).isEqualTo( mapOf( diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt index d44f9b7fb..c10d6ed30 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreTestBase.kt @@ -18,6 +18,7 @@ package com.adobe.testing.s3mock.store import com.adobe.testing.s3mock.dto.ChecksumAlgorithm import com.adobe.testing.s3mock.dto.ChecksumType +import com.adobe.testing.s3mock.dto.Initiator import com.adobe.testing.s3mock.dto.ObjectOwnership import com.adobe.testing.s3mock.dto.Owner import com.adobe.testing.s3mock.dto.Tag @@ -75,7 +76,8 @@ internal abstract class StoreTestBase { const val ENCODING_GZIP = "gzip" val NO_PREFIX: String? = null const val DEFAULT_CONTENT_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE - val TEST_OWNER: Owner = Owner("s3-mock-file-store", "123") + val TEST_OWNER: Owner = Owner("123") + val TEST_INITIATOR: Initiator = Initiator("s3-mock-file-store", "123") val BUCKET_NAMES = mutableSetOf() } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/util/HeaderUtilTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/util/HeaderUtilTest.kt index d760cb25a..82d5ab615 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/util/HeaderUtilTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/util/HeaderUtilTest.kt @@ -80,7 +80,7 @@ internal class HeaderUtilTest { null, null, null, - Owner("name", 0L.toString()), + Owner(0L.toString()), null, null, ChecksumAlgorithm.SHA256, diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testDeserialization.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testDeserialization.xml index 011198d96..f6ec147d5 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testDeserialization.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testDeserialization.xml @@ -19,7 +19,6 @@ 75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a - mtd@amazon.com diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testSerialization.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testSerialization.xml index 434f78617..a2458a788 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testSerialization.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/AccessControlPolicyTest_testSerialization.xml @@ -19,7 +19,6 @@ 75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a - mtd@amazon.com diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest_testSerialization.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest_testSerialization.xml index c57633fb0..f24986036 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest_testSerialization.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest_testSerialization.xml @@ -19,7 +19,6 @@ 10 - displayName diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultTest_testSerialization.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultTest_testSerialization.xml index caac62f2c..11102e9cb 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultTest_testSerialization.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultTest_testSerialization.xml @@ -32,7 +32,6 @@ STANDARD 10 - displayName SHA256 FULL_OBJECT @@ -45,7 +44,6 @@ STANDARD 11 - displayName SHA256 FULL_OBJECT diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultV2Test_testSerialization.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultV2Test_testSerialization.xml index 19b6aaa36..24b7be4f5 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultV2Test_testSerialization.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListBucketResultV2Test_testSerialization.xml @@ -29,7 +29,6 @@ STANDARD 10 - displayName SHA256 FULL_OBJECT @@ -42,7 +41,6 @@ STANDARD 11 - displayName SHA256 FULL_OBJECT diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest_testSerialization.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest_testSerialization.xml index 4d69636d9..fbc85468b 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest_testSerialization.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest_testSerialization.xml @@ -34,7 +34,6 @@ uploadId0 10 - displayName100 100 @@ -50,7 +49,6 @@ uploadId1 11 - displayName101 101 diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListPartsResultTest_testSerialization.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListPartsResultTest_testSerialization.xml index 536bd6d9a..e0762097f 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/ListPartsResultTest_testSerialization.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/ListPartsResultTest_testSerialization.xml @@ -29,7 +29,6 @@ 1000 100 - id displayName diff --git a/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testDeserialization.json b/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testDeserialization.json index 775a865a7..69aae9181 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testDeserialization.json +++ b/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testDeserialization.json @@ -25,7 +25,6 @@ "RetainUntilDate": "2025-09-12T22:50:49.038Z" }, "owner": { - "DisplayName": "s3-mock-file-store", "ID": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be" }, "storeHeaders": { @@ -54,7 +53,6 @@ } ], "Owner": { - "DisplayName": "s3-mock-file-store", "ID": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be" } }, diff --git a/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testSerialization.json b/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testSerialization.json index ad61d21a0..1e07e4344 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testSerialization.json +++ b/server/src/test/resources/com/adobe/testing/s3mock/store/S3ObjectMetadataTest_testSerialization.json @@ -25,7 +25,6 @@ "RetainUntilDate": "2025-09-12T22:50:49.038Z" }, "owner": { - "DisplayName": "s3-mock-file-store", "ID": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be" }, "storeHeaders": { @@ -54,7 +53,6 @@ } ], "Owner": { - "DisplayName": "s3-mock-file-store", "ID": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be" } }, diff --git a/testsupport/common/pom.xml b/testsupport/common/pom.xml index 514787ac6..0d86ba730 100644 --- a/testsupport/common/pom.xml +++ b/testsupport/common/pom.xml @@ -43,15 +43,21 @@ software.amazon.awssdk url-connection-client - - org.jspecify - jspecify - org.assertj assertj-core test + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + test + + + org.jetbrains.kotlin + kotlin-test-junit + test + org.junit.jupiter @@ -68,6 +74,11 @@ junit-jupiter-params test + + org.mockito.kotlin + mockito-kotlin + test + software.amazon.awssdk @@ -80,21 +91,6 @@ aws-xml-protocol test - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - test - - - org.jetbrains.kotlin - kotlin-test-junit - test - - - org.mockito.kotlin - mockito-kotlin - test - diff --git a/testsupport/junit5/pom.xml b/testsupport/junit5/pom.xml index cf353db05..900c4b2ae 100644 --- a/testsupport/junit5/pom.xml +++ b/testsupport/junit5/pom.xml @@ -45,12 +45,6 @@ assertj-core test - - - org.junit.jupiter - junit-jupiter-engine - test - org.jetbrains.kotlin kotlin-stdlib-jdk8 @@ -61,6 +55,12 @@ kotlin-test-junit test + + + org.junit.jupiter + junit-jupiter-engine + test + org.mockito.kotlin mockito-kotlin diff --git a/testsupport/pom.xml b/testsupport/pom.xml index e032eaaaa..01e2608ec 100644 --- a/testsupport/pom.xml +++ b/testsupport/pom.xml @@ -38,13 +38,13 @@ - - maven-checkstyle-plugin - com.github.gantsign.maven ktlint-maven-plugin + + maven-checkstyle-plugin + maven-deploy-plugin diff --git a/testsupport/testcontainers/pom.xml b/testsupport/testcontainers/pom.xml index 6e05720a5..2bc8bc009 100644 --- a/testsupport/testcontainers/pom.xml +++ b/testsupport/testcontainers/pom.xml @@ -31,39 +31,55 @@ S3Mock - Testsupport - Testcontainers + + org.jetbrains.kotlin + kotlin-stdlib + + + org.testcontainers + testcontainers + + + com.adobe.testing + s3mock + test + com.adobe.testing s3mock-docker pom + test * * - test - org.testcontainers - testcontainers + org.assertj + assertj-core + test org.jetbrains.kotlin - kotlin-stdlib + kotlin-stdlib-jdk8 + test - org.jspecify - jspecify + org.jetbrains.kotlin + kotlin-test-junit + test - com.adobe.testing - s3mock + org.junit.jupiter + junit-jupiter test - org.assertj - assertj-core + org.mockito.kotlin + mockito-kotlin test @@ -71,11 +87,6 @@ testcontainers-junit-jupiter test - - org.junit.jupiter - junit-jupiter - test - software.amazon.awssdk aws-query-protocol @@ -96,21 +107,6 @@ url-connection-client test - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - test - - - org.jetbrains.kotlin - kotlin-test-junit - test - - - org.mockito.kotlin - mockito-kotlin - test - diff --git a/testsupport/testng/pom.xml b/testsupport/testng/pom.xml index 18be1fa8d..ca7b8b304 100644 --- a/testsupport/testng/pom.xml +++ b/testsupport/testng/pom.xml @@ -39,10 +39,6 @@ org.testng testng - - org.jspecify - jspecify - org.assertj assertj-core