diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ac121e3..1e67503ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ * [Planned changes](#planned-changes) * [CURRENT - 3.x - THIS VERSION IS UNDER ACTIVE DEVELOPMENT](#current---3x---this-version-is-under-active-development) * [3.10.0 - PLANNED](#3100---planned) - * [3.9.0 - PLANNED](#390---planned) + * [3.9.0](#390) * [3.8.0](#380) * [3.7.3](#373) * [3.7.2](#372) @@ -131,6 +131,7 @@ Version 3.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav * Features and fixes * Support Versions in APIs * Add "DeleteObjectTagging" API + * Support "Ownership" in buckets. ACLs should be * Refactorings * TBD * Version updates @@ -142,13 +143,22 @@ Version 3.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav * Features and fixes * Persist metadata for parts, validate checksum on multipart completion (fixes #1205) * Refactorings - * Migrate Unit tests to Kotlin + * Migrate Unit tests to Kotlin + * Run ITs against real S3, fix code or tests in case of errors + * Fix Checksums for Multiparts + * Add ObjectOwnership config for Buckets, setting ACLs is not allowed otherwise + * Fix StorageClass, it's not returned for most APIs if it's "STANDARD" * Version updates - * Bump aws-v2.version from 2.25.49 to 2.25.54 - * Bump com.amazonaws:aws-java-sdk-s3 from 1.12.720 to 1.12.724 - * Bump actions/checkout from 4.1.5 to 4.1.6 - * Bump github/codeql-action from 3.25.4 to 3.25.5 + * Bump aws-v2.version from 2.25.49 to 2.25.59 + * Bump com.amazonaws:aws-java-sdk-s3 from 1.12.720 to 1.12.729 + * Bump kotlin.version from 1.9.24 to 2.0.0 + * Bump alpine from 3.19.1 to 3.20.0 in /docker + * Bump org.codehaus.mojo:exec-maven-plugin from 3.2.0 to 3.3.0 + * Bump com.github.ekryd.sortpom:sortpom-maven-plugin from 3.4.1 to 4.0.0 * Bump license-maven-plugin-git.version from 4.4 to 4.5 + * Bump actions/checkout from 4.1.5 to 4.1.6 + * Bump github/codeql-action from 3.25.4 to 3.25.6 + * Bump step-security/harden-runner from 2.7.1 to 2.8.0 ## 3.8.0 3.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration. diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 72bbaf3d0..abfabb26a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -47,6 +47,11 @@ + + com.amazonaws + aws-java-sdk-core + test + com.amazonaws aws-java-sdk-s3 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/AclITV2.kt similarity index 88% rename from integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt rename to integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclITV2.kt index bbb229c35..840fabfc4 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/AclITV2.kt @@ -22,22 +22,35 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInfo import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.AccessControlPolicy +import software.amazon.awssdk.services.s3.model.CreateBucketRequest import software.amazon.awssdk.services.s3.model.GetObjectAclRequest import software.amazon.awssdk.services.s3.model.Grant import software.amazon.awssdk.services.s3.model.Grantee import software.amazon.awssdk.services.s3.model.ObjectCannedACL +import software.amazon.awssdk.services.s3.model.ObjectOwnership import software.amazon.awssdk.services.s3.model.Owner import software.amazon.awssdk.services.s3.model.Permission.FULL_CONTROL import software.amazon.awssdk.services.s3.model.PutObjectAclRequest import software.amazon.awssdk.services.s3.model.Type.CANONICAL_USER -internal class AclIT : S3TestBase() { +internal class AclITV2 : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test + @S3VerifiedSuccess(year = 2024) fun testPutCannedAcl_OK(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME - val (bucketName, _) = givenBucketAndObjectV2(testInfo, sourceKey) + val bucketName = bucketName(testInfo) + + //create bucket that sets ownership to non-default to allow setting ACLs. + s3ClientV2.createBucket(CreateBucketRequest + .builder() + .bucket(bucketName) + .objectOwnership(ObjectOwnership.OBJECT_WRITER) + .build() + ) + + givenObjectV2(bucketName, sourceKey) s3ClientV2.putObjectAcl( PutObjectAclRequest @@ -58,8 +71,8 @@ internal class AclIT : S3TestBase() { .build() ).also { assertThat(it.sdkHttpResponse().isSuccessful).isTrue() - assertThat(it.owner().id()).isEqualTo(DEFAULT_OWNER.id) - assertThat(it.owner().displayName()).isEqualTo(DEFAULT_OWNER.displayName) + assertThat(it.owner().id()).isNotBlank() + assertThat(it.owner().displayName()).isNotBlank() assertThat(it.grants().size).isEqualTo(1) assertThat(it.grants()[0].permission()).isEqualTo(FULL_CONTROL) } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV1IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV1IT.kt index 1977a606a..462fd3f17 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV1IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV1IT.kt @@ -75,7 +75,7 @@ internal class BucketV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCreateAndDeleteBucket(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3Client.createBucket(bucketName) @@ -88,7 +88,7 @@ internal class BucketV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testFailureDeleteNonEmptyBucket(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3Client.createBucket(bucketName) @@ -101,7 +101,7 @@ internal class BucketV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testBucketDoesExistV2_ok(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3Client.createBucket(bucketName) @@ -112,7 +112,7 @@ internal class BucketV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testBucketDoesExistV2_failure(testInfo: TestInfo) { val bucketName = bucketName(testInfo) @@ -121,7 +121,7 @@ internal class BucketV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun duplicateBucketCreation(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3Client.createBucket(bucketName) @@ -135,7 +135,7 @@ internal class BucketV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun duplicateBucketDeletion(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3Client.createBucket(bucketName) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt index c0290047f..446731a09 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketV2IT.kt @@ -48,7 +48,7 @@ internal class BucketV2IT : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun createAndDeleteBucket(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3ClientV2.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()) @@ -71,7 +71,7 @@ internal class BucketV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun getBucketLocation(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val bucketLocation = s3ClientV2.getBucketLocation(GetBucketLocationRequest.builder().bucket(bucketName).build()) @@ -80,7 +80,7 @@ internal class BucketV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun duplicateBucketCreation(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3ClientV2.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()) @@ -112,7 +112,7 @@ internal class BucketV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun duplicateBucketDeletion(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3ClientV2.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()) @@ -143,7 +143,7 @@ internal class BucketV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun getBucketLifecycle_notFound(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3ClientV2.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()) @@ -167,7 +167,7 @@ internal class BucketV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun putGetDeleteBucketLifecycle(testInfo: TestInfo) { val bucketName = bucketName(testInfo) s3ClientV2.createBucket(CreateBucketRequest.builder().bucket(bucketName).build()) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV1IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV1IT.kt index d778959fc..d056a259c 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV1IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV1IT.kt @@ -47,7 +47,7 @@ internal class CopyObjectV1IT : S3TestBase() { * compares checksums of original and copied object. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObject(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -65,7 +65,7 @@ internal class CopyObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_successMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -85,7 +85,7 @@ internal class CopyObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_successNoneMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -105,7 +105,7 @@ internal class CopyObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_failureMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -127,7 +127,7 @@ internal class CopyObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_failureNoneMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -153,7 +153,7 @@ internal class CopyObjectV1IT : S3TestBase() { * Downloads the object; compares checksums of original and copied object. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectToSameKey(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -164,16 +164,19 @@ internal class CopyObjectV1IT : S3TestBase() { val putObjectResult = PutObjectRequest(bucketName, sourceKey, uploadFile).withMetadata(objectMetadata).let { s3Client.putObject(it) } - //TODO: this is actually illegal on S3. when copying to the same key like this, S3 will throw: - // This copy request is illegal because it is trying to copy an object to itself without - // changing the object's metadata, storage class, website redirect location or encryption attributes. - CopyObjectRequest(bucketName, sourceKey, bucketName, sourceKey).also { + + CopyObjectRequest(bucketName, sourceKey, bucketName, sourceKey).apply { + this.newObjectMetadata = ObjectMetadata().apply { + this.userMetadata = mapOf("test-key1" to "test-value1") + } + }.also { s3Client.copyObject(it) } s3Client.getObject(bucketName, sourceKey).use { val copiedObjectMetadata = it.objectMetadata - assertThat(copiedObjectMetadata.userMetadata["test-key"]).isEqualTo("test-value") + assertThat(copiedObjectMetadata.userMetadata["test-key"]).isNull() + assertThat(copiedObjectMetadata.userMetadata["test-key1"]).isEqualTo("test-value1") val objectContent = it.objectContent val copiedDigest = DigestUtil.hexDigest(objectContent) @@ -186,7 +189,7 @@ internal class CopyObjectV1IT : S3TestBase() { * Downloads the object; compares checksums of original and copied object. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectWithReplaceToSameKey(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -231,7 +234,7 @@ internal class CopyObjectV1IT : S3TestBase() { * the new user metadata specified during copy request. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectWithNewUserMetadata(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -261,7 +264,7 @@ internal class CopyObjectV1IT : S3TestBase() { * the source object user metadata; */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectWithSourceUserMetadata(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -292,7 +295,7 @@ internal class CopyObjectV1IT : S3TestBase() { * @see .shouldCopyObject */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectToKeyNeedingEscaping(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -316,7 +319,7 @@ internal class CopyObjectV1IT : S3TestBase() { * @see .shouldCopyObject */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectFromKeyNeedingEscaping(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -368,7 +371,7 @@ internal class CopyObjectV1IT : S3TestBase() { * Tests that an object won't be copied with wrong encryption Key. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldNotObjectCopyWithWrongEncryptionKey(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -387,7 +390,7 @@ internal class CopyObjectV1IT : S3TestBase() { * Tests that a copy request for a non-existing object throws the correct error. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldThrowNoSuchKeyOnCopyForNonExistingKey(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val sourceKey = randomName @@ -401,7 +404,7 @@ internal class CopyObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun multipartCopy() { //content larger than default part threshold of 5MiB val contentLen = 10 * _1MB diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV2IT.kt index eedf00e96..fd66cfd3b 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectV2IT.kt @@ -45,7 +45,7 @@ internal class CopyObjectV2IT : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV2(testInfo, sourceKey) @@ -72,7 +72,7 @@ internal class CopyObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_successMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV2(testInfo, sourceKey) @@ -101,7 +101,7 @@ internal class CopyObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_successNoneMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV2(testInfo, sourceKey) @@ -129,7 +129,7 @@ internal class CopyObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_failureMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV2(testInfo, sourceKey) @@ -153,7 +153,7 @@ internal class CopyObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObject_failureNoneMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV2(testInfo, sourceKey) @@ -177,7 +177,7 @@ internal class CopyObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCopyObjectToSameBucketAndKey(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -238,7 +238,48 @@ internal class CopyObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) + fun testCopyObjectToSameBucketAndKey_throws(testInfo: TestInfo) { + val bucketName = givenBucketV2(testInfo) + val uploadFile = File(UPLOAD_FILE_NAME) + val sourceKey = UPLOAD_FILE_NAME + s3ClientV2.putObject(PutObjectRequest + .builder() + .bucket(bucketName) + .key(sourceKey) + .metadata(mapOf("test-key" to "test-value")) + .build(), + RequestBody.fromFile(uploadFile) + ) + val sourceLastModified = s3ClientV2.headObject( + HeadObjectRequest + .builder() + .bucket(bucketName) + .key(sourceKey) + .build() + ).lastModified() + + await("wait until source object is 5 seconds old").until { + sourceLastModified.plusSeconds(5).isBefore(Instant.now()) + } + + assertThatThrownBy { + s3ClientV2.copyObject( + CopyObjectRequest + .builder() + .sourceBucket(bucketName) + .sourceKey(sourceKey) + .destinationBucket(bucketName) + .destinationKey(sourceKey) + .build() + ) + }.isInstanceOf(S3Exception::class.java) + .hasMessageContaining("Service: S3, Status Code: 400") + .hasMessageContaining("This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.") + } + + @Test + @S3VerifiedSuccess(year = 2024) fun testCopyObjectWithNewMetadata(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV2(testInfo, sourceKey) @@ -271,7 +312,7 @@ internal class CopyObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testCopyObject_storageClass(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val uploadFile = File(UPLOAD_FILE_NAME) @@ -281,7 +322,7 @@ internal class CopyObjectV2IT : S3TestBase() { PutObjectRequest.builder() .bucket(bucketName) .key(sourceKey) - .storageClass(StorageClass.DEEP_ARCHIVE) + .storageClass(StorageClass.REDUCED_REDUNDANCY) .build(), RequestBody.fromFile(uploadFile) ) @@ -295,6 +336,8 @@ internal class CopyObjectV2IT : S3TestBase() { .sourceKey(sourceKey) .destinationBucket(destinationBucketName) .destinationKey(destinationKey) + //must set storage class other than "STANDARD" to it gets applied. + .storageClass(StorageClass.STANDARD_IA) .build()) s3ClientV2.getObject(GetObjectRequest @@ -303,7 +346,7 @@ internal class CopyObjectV2IT : S3TestBase() { .key(destinationKey) .build() ).use { - assertThat(it.response().storageClass()).isEqualTo(StorageClass.DEEP_ARCHIVE) + assertThat(it.response().storageClass()).isEqualTo(StorageClass.STANDARD_IA) } } } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsV2IT.kt index 9d4042ddf..c804d44f8 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsV2IT.kt @@ -39,7 +39,8 @@ internal class CorsV2IT : S3TestBase() { private val httpClient: CloseableHttpClient = HttpClients.createDefault() @Test - @S3VerifiedTodo + @S3VerifiedFailure(year = 2024, + reason = "No credentials sent in plain HTTP request") fun testPutObject_cors(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val httpclient = HttpClientBuilder.create().build() @@ -72,6 +73,8 @@ internal class CorsV2IT : S3TestBase() { } @Test + @S3VerifiedFailure(year = 2024, + reason = "No credentials sent in plain HTTP request") fun testGetBucket_cors(testInfo: TestInfo) { val targetBucket = givenBucketV2(testInfo) val httpOptions = HttpOptions("/$targetBucket").apply { diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncV2IT.kt index f74c556df..0a23fb43e 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncV2IT.kt @@ -43,7 +43,7 @@ internal class CrtAsyncV2IT : S3TestBase() { private val autoS3CrtAsyncClientV2: S3AsyncClient = createAutoS3CrtAsyncClientV2() @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutObject_etagCreation(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val uploadFileIs: InputStream = FileInputStream(uploadFile) @@ -72,7 +72,7 @@ internal class CrtAsyncV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutGetObject_successWithMatchingEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val bucketName = randomName @@ -103,7 +103,7 @@ internal class CrtAsyncV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV1IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV1IT.kt index c389cd27d..3cff2fd53 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV1IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV1IT.kt @@ -47,7 +47,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { private val transferManagerV1: TransferManager = createTransferManagerV1() @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun getObject_noSuchKey(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val getObjectRequest = GetObjectRequest(bucketName, NON_EXISTING_KEY) @@ -57,7 +57,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun getObject_noSuchKey_startingSlash(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val getObjectRequest = GetObjectRequest(bucketName, "/$NON_EXISTING_KEY") @@ -67,7 +67,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun putObject_noSuchBucket() { val uploadFile = File(UPLOAD_FILE_NAME) assertThatThrownBy { @@ -84,7 +84,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun putObjectEncrypted_noSuchBucket() { val uploadFile = File(UPLOAD_FILE_NAME) PutObjectRequest(randomName, UPLOAD_FILE_NAME, uploadFile).apply { @@ -104,7 +104,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun copyObjectToNonExistingDestination_noSuchBucket(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) @@ -117,7 +117,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun copyObjectEncryptedToNonExistingDestination_noSuchBucket(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -132,7 +132,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun getObjectMetadata_noSuchBucket() { assertThatThrownBy { s3Client.getObjectMetadata( @@ -145,7 +145,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun deleteFrom_noSuchBucket() { assertThatThrownBy { s3Client.deleteObject( @@ -158,14 +158,14 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun deleteObject_nonExistent_OK(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) s3Client.deleteObject(bucketName, randomName) } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun batchDeleteObjects_noSuchBucket() { val multiObjectDeleteRequest = DeleteObjectsRequest(randomName).apply { this.keys = listOf(KeyVersion("1_$UPLOAD_FILE_NAME")) @@ -176,7 +176,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun deleteBucket_noSuchBucket() { assertThatThrownBy { s3Client.deleteBucket(randomName) } .isInstanceOf(AmazonS3Exception::class.java) @@ -184,7 +184,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun listObjects_noSuchBucket() { assertThatThrownBy { s3Client.listObjects( @@ -197,7 +197,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun uploadParallel_noSuchBucket() { val uploadFile = File(UPLOAD_FILE_NAME) assertThatThrownBy { @@ -211,7 +211,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun multipartUploads_noSuchBucket() { assertThatThrownBy { s3Client.initiateMultipartUpload( @@ -223,7 +223,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun listMultipartUploads_noSuchBucket() { assertThatThrownBy { s3Client.listMultipartUploads( @@ -235,7 +235,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun abortMultipartUpload_noSuchBucket() { assertThatThrownBy { s3Client.abortMultipartUpload( @@ -251,7 +251,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun uploadMultipart_invalidPartNumber(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -281,7 +281,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun completeMultipartUploadWithNonExistingPartNumber(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -317,7 +317,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @Throws(Exception::class) fun rangeDownloadsFromNonExistingBucket() { val transferManager = createTransferManagerV1() @@ -333,7 +333,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @Throws(Exception::class) fun rangeDownloadsFromNonExistingObject(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) @@ -352,7 +352,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @Throws(InterruptedException::class) fun multipartCopyToNonExistingBucket(testInfo: TestInfo) { val sourceBucket = givenBucketV1(testInfo) @@ -385,7 +385,7 @@ internal class ErrorResponsesV1IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @Throws(InterruptedException::class) fun multipartCopyNonExistingObject(testInfo: TestInfo) { val sourceBucket = givenBucketV1(testInfo) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV2IT.kt index 2d597454a..39c44f99d 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ErrorResponsesV2IT.kt @@ -42,7 +42,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun getObject_noSuchKey(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val req = GetObjectRequest.builder().bucket(bucketName).key(NON_EXISTING_KEY).build() @@ -53,7 +53,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun getObject_noSuchKey_startingSlash(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val req = GetObjectRequest.builder().bucket(bucketName).key("/$NON_EXISTING_KEY").build() @@ -64,7 +64,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun putObject_noSuchBucket() { val uploadFile = File(UPLOAD_FILE_NAME) @@ -83,7 +83,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun copyObjectToNonExistingDestination_noSuchBucket(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV2(testInfo, UPLOAD_FILE_NAME) @@ -104,7 +104,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun deleteObject_noSuchBucket() { assertThatThrownBy { s3ClientV2.deleteObject( @@ -120,7 +120,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun deleteObject_nonExistent_OK(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) @@ -134,7 +134,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun deleteObjects_noSuchBucket() { assertThatThrownBy { s3ClientV2.deleteObjects( @@ -157,7 +157,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun deleteBucket_noSuchBucket() { assertThatThrownBy { s3ClientV2.deleteBucket( @@ -172,7 +172,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun multipartUploads_noSuchBucket() { assertThatThrownBy { s3ClientV2.createMultipartUpload( @@ -188,7 +188,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun listMultipartUploads_noSuchBucket() { assertThatThrownBy { s3ClientV2.listMultipartUploads( @@ -203,7 +203,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun abortMultipartUpload_noSuchBucket() { assertThatThrownBy { s3ClientV2.abortMultipartUpload( @@ -220,7 +220,7 @@ internal class ErrorResponsesV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun uploadMultipart_invalidPartNumber(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV1IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV1IT.kt index fd148f6fd..271a4087f 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV1IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV1IT.kt @@ -16,6 +16,7 @@ package com.adobe.testing.s3mock.its import com.adobe.testing.s3mock.util.DigestUtil.hexDigest +import com.amazonaws.HttpMethod import com.amazonaws.services.s3.AmazonS3 import com.amazonaws.services.s3.Headers import com.amazonaws.services.s3.model.AmazonS3Exception @@ -31,7 +32,6 @@ import com.amazonaws.services.s3.model.ResponseHeaderOverrides import com.amazonaws.services.s3.model.SSEAwsKeyManagementParams import com.amazonaws.services.s3.transfer.TransferManager import org.apache.http.HttpHost -import org.apache.http.HttpResponse import org.apache.http.client.methods.HttpGet import org.apache.http.impl.client.HttpClients import org.assertj.core.api.Assertions.assertThat @@ -47,6 +47,7 @@ import java.io.FileInputStream import java.io.InputStream import java.util.UUID import java.util.stream.Collectors +import javax.net.ssl.HostnameVerifier import kotlin.math.min /** @@ -58,7 +59,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { private val transferManagerV1: TransferManager = createTransferManagerV1() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun putObjectWhereKeyContainsPathFragments(testInfo: TestInfo) { val (bucketName, _) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) val objectExist = s3Client.doesObjectExist(bucketName, UPLOAD_FILE_NAME) @@ -70,7 +71,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { */ @ParameterizedTest(name = ParameterizedTest.INDEX_PLACEHOLDER + " uploadWithSigning={0}, uploadChunked={1}") @CsvSource(value = ["true, true", "true, false", "false, true", "false, false"]) - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldUploadAndDownloadObject(uploadWithSigning: Boolean, uploadChunked: Boolean, testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) @@ -91,7 +92,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldTolerateWeirdCharactersInObjectKey(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -108,7 +109,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Stores a file in a previously created bucket. Downloads the file again and compares checksums */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldUploadAndDownloadStream(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val resourceId = UUID.randomUUID().toString() @@ -135,7 +136,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if Object can be uploaded with KMS and Metadata can be retrieved. */ @Test - @S3VerifiedFailure(year = 2022, + @S3VerifiedFailure(year = 2024, reason = "No KMS configuration for AWS test account") fun shouldUploadWithEncryption(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) @@ -162,7 +163,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if Object can be uploaded with wrong KMS Key. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldNotUploadWithWrongEncryptionKey(testInfo: TestInfo) { Configuration().apply { this.setMaxStackTraceElementsDisplayed(10000) @@ -185,7 +186,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if Object can be uploaded with wrong KMS Key. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldNotUploadStreamingWithWrongEncryptionKey(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val bytes = UPLOAD_FILE_NAME.toByteArray() @@ -210,7 +211,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if the Metadata of an existing file can be retrieved. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldGetObjectMetadata(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val nonExistingFileName = randomName @@ -243,7 +244,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if an object can be deleted. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldDeleteObject(testInfo: TestInfo) { val (bucketName, _) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) s3Client.deleteObject(bucketName, UPLOAD_FILE_NAME) @@ -256,7 +257,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if multiple objects can be deleted. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldBatchDeleteObjects(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile1 = File(UPLOAD_FILE_NAME) @@ -292,7 +293,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if Error is thrown when DeleteObjectsRequest contains nonExisting key. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldThrowOnBatchDeleteObjectsWrongKey(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile1 = File(UPLOAD_FILE_NAME) @@ -318,7 +319,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Tests if an object can be uploaded asynchronously. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldUploadInParallel(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -336,7 +337,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { * Verify that range-downloads work. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun checkRangeDownloads(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -374,7 +375,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObject_successWithMatchingEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) @@ -390,7 +391,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObject_failureWithMatchingEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) @@ -407,7 +408,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObject_successWithNonMatchingEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) @@ -424,7 +425,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObject_failureWithNonMatchingEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) @@ -442,7 +443,7 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun generatePresignedUrlWithResponseHeaderOverrides(testInfo: TestInfo) { val (bucketName, _) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) val presignedUrlRequest = GeneratePresignedUrlRequest(bucketName, UPLOAD_FILE_NAME).apply { @@ -456,14 +457,13 @@ internal class GetPutDeleteObjectV1IT : S3TestBase() { this.expires = "expires" } ) + this.method = HttpMethod.GET } - val resourceUrl = s3Client.generatePresignedUrl(presignedUrlRequest) + val resourceUrl = createS3ClientV1(serviceEndpointHttp).generatePresignedUrl(presignedUrlRequest) HttpClients.createDefault().use { val getObject = HttpGet(resourceUrl.toString()) it.execute( - HttpHost( - host, httpPort - ), getObject + getObject ).also { response -> assertThat(response.getFirstHeader(Headers.CACHE_CONTROL).value).isEqualTo("cacheControl") assertThat(response.getFirstHeader(Headers.CONTENT_DISPOSITION).value).isEqualTo("contentDisposition") diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV2IT.kt index ede234133..4db31a2f2 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectV2IT.kt @@ -30,6 +30,7 @@ import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3AsyncClient import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm +import software.amazon.awssdk.services.s3.model.ChecksumMode import software.amazon.awssdk.services.s3.model.GetObjectAttributesRequest import software.amazon.awssdk.services.s3.model.GetObjectRequest import software.amazon.awssdk.services.s3.model.HeadObjectRequest @@ -62,6 +63,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { * * https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html */ + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["charsSafe", "charsSpecial", "charsToAvoid"]) fun testPutHeadGetObject_keyNames_safe(key: String, testInfo: TestInfo) { @@ -81,9 +83,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { .bucket(bucketName) .key(key) .build() - ).also { - assertThat(it.storageClass()).isEqualTo(StorageClass.STANDARD) - } + ) s3ClientV2.getObject( GetObjectRequest.builder() @@ -95,7 +95,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["storageClasses"]) fun testPutObject_storageClass(storageClass: StorageClass, testInfo: TestInfo) { @@ -129,11 +129,17 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { .build() ).use { assertThat(it.response().eTag()).isEqualTo(eTag) + if (storageClass == StorageClass.STANDARD) { + //storageClass STANDARD is never returned from S3 APIs... + assertThat(it.response().storageClass()).isNull() + } else { + assertThat(it.response().storageClass()).isEqualTo(storageClass) + } } } + @S3VerifiedSuccess(year = 2024) @ParameterizedTest - @S3VerifiedTodo @MethodSource(value = ["testFileNames"]) fun testPutObject_etagCreation_sync(testFileName: String, testInfo: TestInfo) { testEtagCreation(testFileName, s3ClientV2, testInfo) @@ -161,8 +167,8 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } } + @S3VerifiedSuccess(year = 2024) @ParameterizedTest - @S3VerifiedTodo @MethodSource(value = ["testFileNames"]) fun testPutObject_etagCreation_async(testFileName: String) { testEtagCreation(testFileName, s3AsyncClientV2) @@ -195,7 +201,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutObject_getObjectAttributes(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val expectedChecksum = DigestUtil.checksumFor(uploadFile.toPath(), Algorithm.SHA1) @@ -220,14 +226,16 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { ObjectAttributes.CHECKSUM) .build() ).also { - assertThat(it.eTag()).isEqualTo(eTag) + // + assertThat(it.eTag()).isEqualTo(eTag.trim('"')) + //default storageClass is STANDARD, which is never returned from APIs assertThat(it.storageClass()).isEqualTo(StorageClass.STANDARD) assertThat(it.objectSize()).isEqualTo(File(UPLOAD_FILE_NAME).length()) assertThat(it.checksum().checksumSHA1()).isEqualTo(expectedChecksum) } } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["checksumAlgorithms"]) fun testPutObject_checksumAlgorithm_http(checksumAlgorithm: ChecksumAlgorithm) { @@ -240,7 +248,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["checksumAlgorithms"]) fun testPutObject_checksumAlgorithm_https(checksumAlgorithm: ChecksumAlgorithm) { @@ -276,6 +284,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { GetObjectRequest.builder() .bucket(bucketName) .key(testFileName) + .checksumMode(ChecksumMode.ENABLED) .build() ).use { val getChecksum = it.response().checksum(checksumAlgorithm) @@ -287,6 +296,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { HeadObjectRequest.builder() .bucket(bucketName) .key(testFileName) + .checksumMode(ChecksumMode.ENABLED) .build() ).also { val headChecksum = it.checksum(checksumAlgorithm) @@ -295,7 +305,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["checksumAlgorithms"]) fun testPutObject_checksumAlgorithm_async_http(checksumAlgorithm: ChecksumAlgorithm) { @@ -315,7 +325,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { testChecksumAlgorithm_async(TEST_IMAGE_LARGE, checksumAlgorithm, autoS3CrtAsyncClientV2Http) } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["checksumAlgorithms"]) fun testPutObject_checksumAlgorithm_async_https(checksumAlgorithm: ChecksumAlgorithm) { @@ -361,6 +371,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { GetObjectRequest.builder() .bucket(bucketName) .key(testFileName) + .checksumMode(ChecksumMode.ENABLED) .build() ).use { val getChecksum = it.response().checksum(checksumAlgorithm) @@ -372,6 +383,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { HeadObjectRequest.builder() .bucket(bucketName) .key(testFileName) + .checksumMode(ChecksumMode.ENABLED) .build() ).also { val headChecksum = it.checksum(checksumAlgorithm) @@ -390,7 +402,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { else -> error("Unknown checksum algorithm") } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["checksumAlgorithms"]) fun testPutObject_checksum(checksumAlgorithm: ChecksumAlgorithm, testInfo: TestInfo) { @@ -406,7 +418,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { .build(), RequestBody.fromFile(uploadFile) ).also { - val putChecksum = it.checksum(checksumAlgorithm) + val putChecksum = it.checksum(checksumAlgorithm)!! assertThat(putChecksum).isNotBlank assertThat(putChecksum).isEqualTo(expectedChecksum) } @@ -415,6 +427,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { GetObjectRequest.builder() .bucket(bucketName) .key(UPLOAD_FILE_NAME) + .checksumMode(ChecksumMode.ENABLED) .build() ).use { val getChecksum = it.response().checksum(checksumAlgorithm) @@ -426,6 +439,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { HeadObjectRequest.builder() .bucket(bucketName) .key(UPLOAD_FILE_NAME) + .checksumMode(ChecksumMode.ENABLED) .build() ).also { val headChecksum = it.checksum(checksumAlgorithm) @@ -435,7 +449,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutObject_wrongChecksum(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val expectedChecksum = "wrongChecksum" @@ -453,7 +467,8 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { ) } .isInstanceOf(S3Exception::class.java) - .hasMessageContaining("The Content-MD5 or checksum value that you specified did not match what the server received.") + .hasMessageContaining("Service: S3, Status Code: 400") + .hasMessageContaining("Value for x-amz-checksum-sha1 header is invalid.") } /** @@ -461,7 +476,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { * https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html */ @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutObject_safeCharacters(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val bucketName = givenBucketV2(testInfo) @@ -500,7 +515,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { * https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html */ @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutObject_specialHandlingCharacters(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val bucketName = givenBucketV2(testInfo) @@ -535,7 +550,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutGetDeleteObject_twoBuckets(testInfo: TestInfo) { val bucket1 = givenRandomBucketV2() val bucket2 = givenRandomBucketV2() @@ -556,7 +571,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutGetHeadObject_storeHeaders(testInfo: TestInfo) { val bucket = givenRandomBucketV2() val uploadFile = File(UPLOAD_FILE_NAME) @@ -609,7 +624,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObject_successWithMatchingEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val matchingEtag = FileInputStream(uploadFile).let { @@ -633,7 +648,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testGetObject_successWithSameLength(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val matchingEtag = FileInputStream(uploadFile).let { @@ -653,7 +668,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObject_successWithMatchingWildcardEtag(testInfo: TestInfo) { val (bucketName, putObjectResponse) = givenBucketAndObjectV2(testInfo, UPLOAD_FILE_NAME) val eTag = putObjectResponse.eTag() @@ -671,7 +686,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testHeadObject_successWithNonMatchEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val expectedEtag = FileInputStream(uploadFile).let { @@ -697,7 +712,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testHeadObject_failureWithNonMatchWildcardEtag(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val expectedEtag = FileInputStream(uploadFile).let { @@ -724,7 +739,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testHeadObject_failureWithMatchEtag(testInfo: TestInfo) { val expectedEtag = FileInputStream(File(UPLOAD_FILE_NAME)).let { "\"${DigestUtil.hexDigest(it)}\"" @@ -750,7 +765,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testGetObject_rangeDownloads(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val (bucketName, putObjectResponse) = givenBucketAndObjectV2(testInfo, UPLOAD_FILE_NAME) @@ -790,7 +805,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testGetObject_rangeDownloads_finalBytes_prefixOffset(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val key = givenObjectV2WithRandomBytes(bucketName) @@ -809,7 +824,7 @@ internal class GetPutDeleteObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testGetObject_rangeDownloads_finalBytes_suffixOffset(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val key = givenObjectV2WithRandomBytes(bucketName) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldV2IT.kt index eacd98a33..b79bc91e4 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldV2IT.kt @@ -16,7 +16,6 @@ package com.adobe.testing.s3mock.its -import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test @@ -37,7 +36,7 @@ internal class LegalHoldV2IT : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetLegalHoldNoBucketLockConfiguration(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV1(testInfo, sourceKey) @@ -56,15 +55,23 @@ internal class LegalHoldV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetLegalHoldNoObjectLockConfiguration(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val sourceKey = UPLOAD_FILE_NAME val bucketName = bucketName(testInfo) - s3ClientV2.createBucket(CreateBucketRequest.builder().bucket(bucketName) - .objectLockEnabledForBucket(true).build()) + s3ClientV2.createBucket(CreateBucketRequest + .builder() + .bucket(bucketName) + .objectLockEnabledForBucket(true) + .build() + ) s3ClientV2.putObject( - PutObjectRequest.builder().bucket(bucketName).key(sourceKey).build(), + PutObjectRequest + .builder() + .bucket(bucketName) + .key(sourceKey) + .build(), RequestBody.fromFile(uploadFile) ) @@ -82,7 +89,7 @@ internal class LegalHoldV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutAndGetLegalHold(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val sourceKey = UPLOAD_FILE_NAME @@ -94,7 +101,11 @@ internal class LegalHoldV2IT : S3TestBase() { .build() ) s3ClientV2.putObject( - PutObjectRequest.builder().bucket(bucketName).key(sourceKey).build(), + PutObjectRequest + .builder() + .bucket(bucketName) + .key(sourceKey) + .build(), RequestBody.fromFile(uploadFile) ) @@ -102,7 +113,11 @@ internal class LegalHoldV2IT : S3TestBase() { .builder() .bucket(bucketName) .key(sourceKey) - .legalHold(ObjectLockLegalHold.builder().status(ObjectLockLegalHoldStatus.ON).build()) + .legalHold(ObjectLockLegalHold + .builder() + .status(ObjectLockLegalHoldStatus.ON) + .build() + ) .build() ) @@ -115,5 +130,27 @@ internal class LegalHoldV2IT : S3TestBase() { ).also { assertThat(it.legalHold().status()).isEqualTo(ObjectLockLegalHoldStatus.ON) } + + s3ClientV2.putObjectLegalHold(PutObjectLegalHoldRequest + .builder() + .bucket(bucketName) + .key(sourceKey) + .legalHold(ObjectLockLegalHold + .builder() + .status(ObjectLockLegalHoldStatus.OFF) + .build() + ) + .build() + ) + + s3ClientV2.getObjectLegalHold( + GetObjectLegalHoldRequest + .builder() + .bucket(bucketName) + .key(sourceKey) + .build() + ).also { + assertThat(it.legalHold().status()).isEqualTo(ObjectLockLegalHoldStatus.OFF) + } } } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1IT.kt index 2a015ebeb..9be2e623f 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1IT.kt @@ -81,7 +81,7 @@ internal class ListObjectV1IT : S3TestBase() { */ @ParameterizedTest @MethodSource("data") - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun listV1(parameters: Param, testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) // create all expected objects @@ -125,7 +125,7 @@ internal class ListObjectV1IT : S3TestBase() { */ @ParameterizedTest @MethodSource("data") - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun listV2(parameters: Param, testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) // create all expected objects @@ -167,7 +167,7 @@ internal class ListObjectV1IT : S3TestBase() { * https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldListWithCorrectObjectNames(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -190,7 +190,7 @@ internal class ListObjectV1IT : S3TestBase() { * https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldListV2WithCorrectObjectNames(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -224,7 +224,7 @@ internal class ListObjectV1IT : S3TestBase() { * is currently no low-level testing infrastructure in place. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldHonorEncodingType(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -247,7 +247,7 @@ internal class ListObjectV1IT : S3TestBase() { * The same as [shouldHonorEncodingType] but for V2 API. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldHonorEncodingTypeV2(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -269,7 +269,7 @@ internal class ListObjectV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldGetObjectListing(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -285,7 +285,7 @@ internal class ListObjectV1IT : S3TestBase() { * Stores files in a previously created bucket. List files using ListObjectsV2Request */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldUploadAndListV2Objects(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1MaxKeysIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1MaxKeysIT.kt index 46ae69afa..bbf3c0101 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1MaxKeysIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1MaxKeysIT.kt @@ -25,7 +25,7 @@ internal class ListObjectV1MaxKeysIT : S3TestBase() { val s3Client: AmazonS3 = createS3ClientV1() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun returnsLimitedAmountOfObjectsBasedOnMaxKeys(testInfo: TestInfo) { val bucketName = givenBucketWithTwoObjects(testInfo) val request = ListObjectsRequest().withBucketName(bucketName).withMaxKeys(1) @@ -37,7 +37,7 @@ internal class ListObjectV1MaxKeysIT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun returnsAllObjectsIfMaxKeysIsDefault(testInfo: TestInfo) { val bucketName = givenBucketWithTwoObjects(testInfo) val request = ListObjectsRequest().withBucketName(bucketName) @@ -49,7 +49,7 @@ internal class ListObjectV1MaxKeysIT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun returnsAllObjectsIfMaxKeysEqualToAmountOfObjects(testInfo: TestInfo) { val bucketName = givenBucketWithTwoObjects(testInfo) val request = ListObjectsRequest().withBucketName(bucketName).withMaxKeys(2) @@ -61,7 +61,7 @@ internal class ListObjectV1MaxKeysIT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun returnsAllObjectsIfMaxKeysMoreThanAmountOfObjects(testInfo: TestInfo) { val bucketName = givenBucketWithTwoObjects(testInfo) val request = ListObjectsRequest().withBucketName(bucketName).withMaxKeys(3) @@ -73,7 +73,7 @@ internal class ListObjectV1MaxKeysIT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun returnsEmptyListIfMaxKeysIsZero(testInfo: TestInfo) { val bucketName = givenBucketWithTwoObjects(testInfo) val request = ListObjectsRequest().withBucketName(bucketName).withMaxKeys(0) @@ -85,7 +85,7 @@ internal class ListObjectV1MaxKeysIT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun returnsAllObjectsIfMaxKeysIsNegative(testInfo: TestInfo) { val bucketName = givenBucketWithTwoObjects(testInfo) val request = ListObjectsRequest().withBucketName(bucketName).withMaxKeys(-1) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1PaginationIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1PaginationIT.kt index c045f95f3..0524c758c 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1PaginationIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV1PaginationIT.kt @@ -25,7 +25,7 @@ internal class ListObjectV1PaginationIT : S3TestBase() { val s3Client: AmazonS3 = createS3ClientV1() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldTruncateAndReturnNextMarker(testInfo: TestInfo) { val bucketName = givenBucketWithTwoObjects(testInfo) val request = ListObjectsRequest().withBucketName(bucketName).withMaxKeys(1) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV2IT.kt index 431e54e1e..68e825b41 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectV2IT.kt @@ -34,7 +34,7 @@ internal class ListObjectV2IT : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutObjectsListObjectsV2_checksumAlgorithm_sha256(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val bucketName = givenBucketV2(testInfo) @@ -71,7 +71,7 @@ internal class ListObjectV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testPutObjectsListObjectsV1_checksumAlgorithm_sha256(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val bucketName = givenBucketV2(testInfo) @@ -113,7 +113,7 @@ internal class ListObjectV2IT : S3TestBase() { * https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html */ @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun shouldListV2WithCorrectObjectNames(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectVersionsV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectVersionsV2IT.kt index e80da6620..aa4fb8a8d 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectVersionsV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectVersionsV2IT.kt @@ -32,6 +32,7 @@ internal class ListObjectVersionsV2IT : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test + @S3VerifiedSuccess(year = 2024) fun testPutObjects_listObjectVersions(testInfo: TestInfo) { val uploadFile = File(UPLOAD_FILE_NAME) val bucketName = givenBucketV2(testInfo) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV1IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV1IT.kt index fa6bb8ae2..b5f33bb58 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV1IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV1IT.kt @@ -49,7 +49,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests if user metadata can be passed by multipart upload. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload_withUserMetadata(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -93,7 +93,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests if a multipart upload with the last part being smaller than 5MB works. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldAllowMultipartUploads(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -147,7 +147,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldInitiateMultipartAndRetrieveParts(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -193,7 +193,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests if not yet completed / aborted multipart uploads are listed. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldListMultipartUploads(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) assertThat( @@ -220,7 +220,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests if empty parts list of not yet completed multipart upload is returned. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldListEmptyPartListForMultipartUpload(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) assertThat( @@ -244,7 +244,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests that an exception is thrown when listing parts if the upload id is unknown. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldThrowOnListMultipartUploadsWithUnknownId(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) assertThatThrownBy { s3Client.listParts(ListPartsRequest(bucketName, "NON_EXISTENT_KEY", @@ -257,7 +257,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests if not yet completed / aborted multipart uploads are listed with prefix filtering. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldListMultipartUploadsWithPrefix(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) s3Client.initiateMultipartUpload( @@ -280,7 +280,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests if multipart uploads are stored and can be retrieved by bucket. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldListMultipartUploadsWithBucket(testInfo: TestInfo) { // create multipart upload 1 val bucketName1 = givenBucketV1(testInfo) @@ -312,7 +312,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tests if a multipart upload can be aborted. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldAbortMultipartUpload(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) assertThat(s3Client.listMultipartUploads(ListMultipartUploadsRequest(bucketName)).multipartUploads).isEmpty() @@ -345,7 +345,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * irrespective of the number of parts uploaded before. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldAdherePartsInCompleteMultipartUploadRequest(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val key = UUID.randomUUID().toString() @@ -392,7 +392,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * aborted. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldListPartsOnCompleteOrAbort(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val key = randomName @@ -427,7 +427,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Upload two objects, copy as parts without length, complete multipart. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyPartsAndComplete(testInfo: TestInfo) { //Initiate upload in random bucket val bucketName2 = givenRandomBucketV1() @@ -492,7 +492,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Requests parts for the uploadId; compares etag of upload response and parts list. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectPart(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val (bucketName, putObjectResult) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) @@ -538,7 +538,7 @@ internal class MultiPartUploadV1IT : S3TestBase() { * Tries to copy part of a non-existing object to a new bucket. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldThrowNoSuchKeyOnCopyObjectPartForNonExistingKey(testInfo: TestInfo) { val sourceKey = "NON_EXISTENT_KEY" val destinationBucketName = givenRandomBucketV1() diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV2IT.kt index 6074949ea..22e47c8c2 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartUploadV2IT.kt @@ -31,12 +31,13 @@ import org.springframework.web.util.UriUtils import software.amazon.awssdk.awscore.exception.AwsErrorDetails import software.amazon.awssdk.awscore.exception.AwsServiceException import software.amazon.awssdk.core.async.AsyncRequestBody -import software.amazon.awssdk.core.checksums.Algorithm +import software.amazon.awssdk.core.checksums.Algorithm.CRC32 import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3AsyncClient import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.AbortMultipartUploadRequest import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm +import software.amazon.awssdk.services.s3.model.ChecksumMode import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload import software.amazon.awssdk.services.s3.model.CompletedPart @@ -69,7 +70,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { private val transferManagerV2: S3TransferManager = createTransferManagerV2() @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload_asyncClient(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -82,7 +83,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { .build(), AsyncRequestBody.fromFile(uploadFile) ).join().also { - assertThat(it.checksumCRC32()).isEqualTo(DigestUtil.checksumFor(uploadFile.toPath(), Algorithm.CRC32)) + assertThat(it.checksumCRC32()).isEqualTo(DigestUtil.checksumFor(uploadFile.toPath(), CRC32)) } s3AsyncClientV2.waiter() @@ -112,7 +113,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload_transferManager(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -167,7 +168,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if user metadata can be passed by multipart upload. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload_withUserMetadata(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -226,7 +227,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if a multipart upload with the last part being smaller than 5MB works. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -313,14 +314,18 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if a multipart upload with the last part being smaller than 5MB works. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload_checksum(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(TEST_IMAGE_TIFF) //construct uploadfile >5MB - val uploadBytes = readStreamIntoByteArray(uploadFile.inputStream()) + - readStreamIntoByteArray(uploadFile.inputStream()) + - readStreamIntoByteArray(uploadFile.inputStream()) + val tempFile = Files.newTemporaryFile().also { + (readStreamIntoByteArray(uploadFile.inputStream()) + + readStreamIntoByteArray(uploadFile.inputStream()) + + readStreamIntoByteArray(uploadFile.inputStream())) + .inputStream() + .copyTo(it.outputStream()) + } val initiateMultipartUploadResult = s3ClientV2 .createMultipartUpload( @@ -332,29 +337,39 @@ internal class MultiPartUploadV2IT : S3TestBase() { ) val uploadId = initiateMultipartUploadResult.uploadId() // upload part 1, <5MB - val etag1 = s3ClientV2.uploadPart( + val partResponse1 = s3ClientV2.uploadPart( UploadPartRequest .builder() .bucket(initiateMultipartUploadResult.bucket()) .key(initiateMultipartUploadResult.key()) .uploadId(uploadId) + .checksumAlgorithm(ChecksumAlgorithm.CRC32) .partNumber(1) - .contentLength(uploadBytes.size.toLong()).build(), + .contentLength(tempFile.length()).build(), //.lastPart(true) - RequestBody.fromBytes(uploadBytes), - ).eTag() + RequestBody.fromFile(tempFile), + ) + val etag1 = partResponse1.eTag() + val checksum1 = partResponse1.checksumCRC32() // upload part 2, <5MB - val etag2 = s3ClientV2.uploadPart( + val partResponse2 = s3ClientV2.uploadPart( UploadPartRequest .builder() .bucket(initiateMultipartUploadResult.bucket()) .key(initiateMultipartUploadResult.key()) .uploadId(uploadId) + .checksumAlgorithm(ChecksumAlgorithm.CRC32) .partNumber(2) .contentLength(uploadFile.length()).build(), //.lastPart(true) RequestBody.fromFile(uploadFile), - ).eTag() + ) + val etag2 = partResponse2.eTag() + val checksum2 = partResponse2.checksumCRC32() + val localChecksum1 = DigestUtil.checksumFor(tempFile.toPath(), CRC32) + assertThat(checksum1).isEqualTo(localChecksum1) + val localChecksum2 = DigestUtil.checksumFor(uploadFile.toPath(), CRC32) + assertThat(checksum2).isEqualTo(localChecksum2) val completeMultipartUpload = s3ClientV2.completeMultipartUpload( CompleteMultipartUploadRequest @@ -370,11 +385,13 @@ internal class MultiPartUploadV2IT : S3TestBase() { .builder() .eTag(etag1) .partNumber(1) + .checksumCRC32(checksum1) .build(), CompletedPart .builder() .eTag(etag2) .partNumber(2) + .checksumCRC32(checksum2) .build() ) .build() @@ -382,9 +399,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { .build() ) - val uploadFileBytes = readStreamIntoByteArray(uploadFile.inputStream()) - - (DigestUtils.md5(uploadBytes) + DigestUtils.md5(readStreamIntoByteArray(uploadFile.inputStream()))).also { + (DigestUtils.md5(tempFile.readBytes()) + DigestUtils.md5(readStreamIntoByteArray(uploadFile.inputStream()))).also { // verify special etag assertThat(completeMultipartUpload.eTag()).isEqualTo("\"${DigestUtils.md5Hex(it)}-2\"") } @@ -394,13 +409,14 @@ internal class MultiPartUploadV2IT : S3TestBase() { .builder() .bucket(bucketName) .key(TEST_IMAGE_TIFF) + .checksumMode(ChecksumMode.ENABLED) .build() ).use { // verify content size - assertThat(it.response().contentLength()).isEqualTo(uploadBytes.size.toLong() + uploadFileBytes.size.toLong()) + assertThat(it.response().contentLength()).isEqualTo(tempFile.length() + uploadFile.length()) // verify contents - assertThat(readStreamIntoByteArray(it.buffered())).isEqualTo(concatByteArrays(uploadBytes, uploadFileBytes)) - assertThat(it.response().checksumCRC32()).isEqualTo("0qCRZA==") //TODO: fix checksum for random byte uploads + assertThat(readStreamIntoByteArray(it.buffered())).isEqualTo(tempFile.readBytes() + uploadFile.readBytes()) + assertThat(it.response().checksumCRC32()).isEqualTo("oGk6qg==-2") } assertThat(completeMultipartUpload.location()) @@ -408,7 +424,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["checksumAlgorithms"]) fun testUploadPart_checksumAlgorithm(checksumAlgorithm: ChecksumAlgorithm, testInfo: TestInfo) { @@ -420,6 +436,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { .builder() .bucket(bucketName) .key(UPLOAD_FILE_NAME) + .checksumAlgorithm(checksumAlgorithm) .build() ) val uploadId = initiateMultipartUploadResult.uploadId() @@ -450,7 +467,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { ) } - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) @ParameterizedTest @MethodSource(value = ["checksumAlgorithms"]) fun testMultipartUpload_checksum(checksumAlgorithm: ChecksumAlgorithm, testInfo: TestInfo) { @@ -462,6 +479,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { .builder() .bucket(bucketName) .key(UPLOAD_FILE_NAME) + .checksumAlgorithm(checksumAlgorithm) .build() ) val uploadId = initiateMultipartUploadResult.uploadId() @@ -489,7 +507,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { } @Test - @S3VerifiedTodo + @S3VerifiedSuccess(year = 2024) fun testMultipartUpload_wrongChecksum(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -519,7 +537,8 @@ internal class MultiPartUploadV2IT : S3TestBase() { ) } .isInstanceOf(S3Exception::class.java) - .hasMessageContaining("The Content-MD5 or checksum value that you specified did not match what the server received.") + .hasMessageContaining("Service: S3, Status Code: 400") + .hasMessageContaining("Value for x-amz-checksum-sha1 header is invalid.") } private fun UploadPartRequest.Builder.checksum( @@ -535,7 +554,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testInitiateMultipartAndRetrieveParts(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -587,7 +606,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if not yet completed / aborted multipart uploads are listed. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testListMultipartUploads_ok(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) assertThat( @@ -628,7 +647,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if empty parts list of not yet completed multipart upload is returned. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testListMultipartUploads_empty(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) assertThat( @@ -669,7 +688,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests that an exception is thrown when listing parts if the upload id is unknown. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testListMultipartUploads_throwOnUnknownId(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) @@ -691,7 +710,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if not yet completed / aborted multipart uploads are listed with prefix filtering. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testListMultipartUploads_withPrefix(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) s3ClientV2 @@ -721,7 +740,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if multipart uploads are stored and can be retrieved by bucket. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testListMultipartUploads_multipleBuckets(testInfo: TestInfo) { // create multipart upload 1 val bucketName1 = givenBucketV2(testInfo) @@ -780,7 +799,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tests if a multipart upload can be aborted. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testAbortMultipartUpload(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) assertThat( @@ -864,7 +883,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * irrespective of the number of parts uploaded before. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testCompleteMultipartUpload_partLeftOut(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val key = randomName @@ -947,7 +966,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * aborted. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testListParts_completeAndAbort(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val key = randomName @@ -1036,7 +1055,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Upload two objects, copy as parts without length, complete multipart. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyPartsAndComplete(testInfo: TestInfo) { //Initiate upload val bucketName2 = givenRandomBucketV2() @@ -1135,7 +1154,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Requests parts for the uploadId; compares etag of upload response and parts list. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldCopyObjectPart(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val uploadFile = File(sourceKey) @@ -1185,7 +1204,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { * Tries to copy part of a non-existing object to a new bucket. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun shouldThrowNoSuchKeyOnCopyObjectPartForNonExistingKey(testInfo: TestInfo) { val sourceKey = "NON_EXISTENT_KEY" val destinationBucket = givenRandomBucketV2() @@ -1272,7 +1291,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testUploadPartCopy_successNoneMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val uploadFile = File(sourceKey) @@ -1319,7 +1338,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testUploadPartCopy_failureMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val uploadFile = File(sourceKey) @@ -1358,7 +1377,7 @@ internal class MultiPartUploadV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testUploadPartCopy_failureNoneMatch(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val uploadFile = File(sourceKey) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV1IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV1IT.kt index 4b01eb3cc..fcf0267e3 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV1IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV1IT.kt @@ -33,7 +33,7 @@ internal class ObjectTaggingV1IT : S3TestBase() { val s3Client: AmazonS3 = createS3ClientV1() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutAndGetObjectTagging(testInfo: TestInfo) { val (bucketName, _) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) val s3Object = s3Client.getObject(bucketName, UPLOAD_FILE_NAME) @@ -52,7 +52,7 @@ internal class ObjectTaggingV1IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutObjectAndGetObjectTagging_withTagging(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -77,7 +77,7 @@ internal class ObjectTaggingV1IT : S3TestBase() { * Verify that tagging with multiple tags can be obtained and returns expected content. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutObjectAndGetObjectTagging_multipleTags(testInfo: TestInfo) { val bucketName = givenBucketV1(testInfo) val uploadFile = File(UPLOAD_FILE_NAME) @@ -101,7 +101,7 @@ internal class ObjectTaggingV1IT : S3TestBase() { @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObjectTagging_noTags(testInfo: TestInfo) { val (bucketName, _) = givenBucketAndObjectV1(testInfo, UPLOAD_FILE_NAME) val s3Object = s3Client.getObject(bucketName, UPLOAD_FILE_NAME) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV2IT.kt index 491936909..acc019d06 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingV2IT.kt @@ -30,7 +30,7 @@ internal class ObjectTaggingV2IT : S3TestBase() { private val s3ClientV2: S3Client = createS3ClientV2() @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testGetObjectTagging_noTags(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) s3ClientV2.putObject( @@ -48,7 +48,7 @@ internal class ObjectTaggingV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutAndGetObjectTagging(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val key = "foo" @@ -77,7 +77,7 @@ internal class ObjectTaggingV2IT : S3TestBase() { } @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutObjectAndGetObjectTagging_withTagging(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) s3ClientV2.putObject( @@ -98,7 +98,7 @@ internal class ObjectTaggingV2IT : S3TestBase() { * Verify that tagging with multiple tags can be obtained and returns expected content. */ @Test - @S3VerifiedSuccess(year = 2022) + @S3VerifiedSuccess(year = 2024) fun testPutObjectAndGetObjectTagging_multipleTags(testInfo: TestInfo) { val bucketName = givenBucketV2(testInfo) val tag1 = Tag.builder().key("tag1").value("foo").build() diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUriV2IT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUrlV2IT.kt similarity index 90% rename from integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUriV2IT.kt rename to integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUrlV2IT.kt index e25e422f5..380fae4ff 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUriV2IT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUrlV2IT.kt @@ -15,8 +15,8 @@ */ package com.adobe.testing.s3mock.its +import com.adobe.testing.s3mock.dto.InitiateMultipartUploadResult import com.adobe.testing.s3mock.util.DigestUtil -import org.apache.http.HttpHost import org.apache.http.HttpStatus import org.apache.http.client.methods.HttpDelete import org.apache.http.client.methods.HttpGet @@ -53,13 +53,14 @@ import java.nio.file.Files import java.nio.file.Path import java.time.Duration -internal class PresignedUriV2IT : S3TestBase() { +internal class PresignedUrlV2IT : S3TestBase() { private val httpClient: CloseableHttpClient = HttpClients.createDefault() private val s3ClientV2: S3Client = createS3ClientV2() - private val s3Presigner: S3Presigner = createS3Presigner() + private val s3Presigner: S3Presigner = createS3Presigner(serviceEndpointHttp) @Test - fun testPresignedUri_getObject(testInfo: TestInfo) { + @S3VerifiedSuccess(year = 2024) + fun testPresignedUrl_getObject(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val (bucketName, _) = givenBucketAndObjectV2(testInfo, key) @@ -82,9 +83,6 @@ internal class PresignedUriV2IT : S3TestBase() { HttpGet(presignedUrlString).also { get -> httpClient.execute( - HttpHost( - host, httpPort - ), get ).use { assertThat(it.statusLine.statusCode).isEqualTo(HttpStatus.SC_OK) @@ -96,7 +94,8 @@ internal class PresignedUriV2IT : S3TestBase() { } @Test - fun testPresignedUri_putObject(testInfo: TestInfo) { + @S3VerifiedSuccess(year = 2024) + fun testPresignedUrl_putObject(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val bucketName = givenBucketV2(testInfo) @@ -121,9 +120,6 @@ internal class PresignedUriV2IT : S3TestBase() { this.entity = FileEntity(File(UPLOAD_FILE_NAME)) }.also { put -> httpClient.execute( - HttpHost( - host, httpPort - ), put ).use { assertThat(it.statusLine.statusCode).isEqualTo(HttpStatus.SC_OK) @@ -143,7 +139,8 @@ internal class PresignedUriV2IT : S3TestBase() { } @Test - fun testPresignedUri_createMultipartUpload(testInfo: TestInfo) { + @S3VerifiedFailure(year = 2024, reason = "S3 returns no multipart uploads.") + fun testPresignedUrl_createMultipartUpload(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val bucketName = givenBucketV2(testInfo) @@ -164,25 +161,16 @@ internal class PresignedUriV2IT : S3TestBase() { val presignedUrlString = presignCreateMultipartUpload.url().toString() assertThat(presignedUrlString).isNotBlank() - HttpPost(presignedUrlString).apply { - this.entity = StringEntity( - """ - - bucketName - fileName - uploadId - - """ - ) - }.also { post -> - httpClient.execute( - HttpHost( - host, httpPort - ), + val uploadId = HttpPost(presignedUrlString) + .let { post -> + httpClient.execute( post ).use { assertThat(it.statusLine.statusCode).isEqualTo(HttpStatus.SC_OK) - } + val result = MAPPER.readValue(it.entity.content, InitiateMultipartUploadResult::class.java) + assertThat(result).isNotNull + result + }.uploadId } s3ClientV2.listMultipartUploads( @@ -190,6 +178,7 @@ internal class PresignedUriV2IT : S3TestBase() { .builder() .bucket(bucketName) .keyMarker(key) + .uploadIdMarker(uploadId) .build() ).also { assertThat(it.uploads()).hasSize(1) @@ -197,7 +186,8 @@ internal class PresignedUriV2IT : S3TestBase() { } @Test - fun testPresignedUri_abortMultipartUpload(testInfo: TestInfo) { + @S3VerifiedSuccess(year = 2024) + fun testPresignedUrl_abortMultipartUpload(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val bucketName = givenBucketV2(testInfo) val file = File(UPLOAD_FILE_NAME) @@ -242,9 +232,6 @@ internal class PresignedUriV2IT : S3TestBase() { HttpDelete(presignedUrlString).also { delete -> httpClient.execute( - HttpHost( - host, httpPort - ), delete ).use { assertThat(it.statusLine.statusCode).isEqualTo(HttpStatus.SC_NO_CONTENT) @@ -263,7 +250,8 @@ internal class PresignedUriV2IT : S3TestBase() { } @Test - fun testPresignedUri_completeMultipartUpload(testInfo: TestInfo) { + @S3VerifiedSuccess(year = 2024) + fun testPresignedUrl_completeMultipartUpload(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val bucketName = givenBucketV2(testInfo) val file = File(UPLOAD_FILE_NAME) @@ -318,9 +306,6 @@ internal class PresignedUriV2IT : S3TestBase() { """) }.also { post -> httpClient.execute( - HttpHost( - host, httpPort - ), post ).use { assertThat(it.statusLine.statusCode).isEqualTo(HttpStatus.SC_OK) @@ -340,7 +325,8 @@ internal class PresignedUriV2IT : S3TestBase() { @Test - fun testPresignedUri_uploadPart(testInfo: TestInfo) { + @S3VerifiedSuccess(year = 2024) + fun testPresignedUrl_uploadPart(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val bucketName = givenBucketV2(testInfo) val file = File(UPLOAD_FILE_NAME) @@ -373,13 +359,10 @@ internal class PresignedUriV2IT : S3TestBase() { val presignedUrlString = presignUploadPart.url().toString() assertThat(presignedUrlString).isNotBlank() - val httpPut = HttpPut(presignedUrlString).apply { + HttpPut(presignedUrlString).apply { this.entity = FileEntity(File(UPLOAD_FILE_NAME)) }.also { put -> httpClient.execute( - HttpHost( - host, httpPort - ), put ).use { assertThat(it.statusLine.statusCode).isEqualTo(HttpStatus.SC_OK) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/S3TestBase.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/S3TestBase.kt index 06723ac43..0f4a08d0d 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/S3TestBase.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/S3TestBase.kt @@ -27,6 +27,7 @@ import com.amazonaws.services.s3.model.PutObjectRequest import com.amazonaws.services.s3.model.PutObjectResult import com.amazonaws.services.s3.transfer.TransferManager import com.amazonaws.services.s3.transfer.TransferManagerBuilder +import com.fasterxml.jackson.dataformat.xml.XmlMapper import org.apache.http.conn.ssl.NoopHostnameVerifier import org.apache.http.conn.ssl.SSLConnectionSocketFactory import org.assertj.core.api.Assertions.assertThat @@ -56,6 +57,7 @@ import software.amazon.awssdk.services.s3.model.DeleteBucketRequest import software.amazon.awssdk.services.s3.model.DeleteObjectRequest import software.amazon.awssdk.services.s3.model.DeleteObjectResponse import software.amazon.awssdk.services.s3.model.EncodingType +import software.amazon.awssdk.services.s3.model.GetObjectAttributesResponse import software.amazon.awssdk.services.s3.model.GetObjectLockConfigurationRequest import software.amazon.awssdk.services.s3.model.GetObjectRequest import software.amazon.awssdk.services.s3.model.GetObjectResponse @@ -352,9 +354,18 @@ internal abstract class S3TestBase { } private fun deleteBucket(bucket: Bucket) { - _s3ClientV2.deleteBucket(DeleteBucketRequest.builder().bucket(bucket.name()).build()) - val bucketDeleted = _s3ClientV2.waiter() - .waitUntilBucketNotExists(HeadBucketRequest.builder().bucket(bucket.name()).build()) + _s3ClientV2.deleteBucket(DeleteBucketRequest + .builder() + .bucket(bucket.name()) + .build() + ) + val bucketDeleted = _s3ClientV2 + .waiter() + .waitUntilBucketNotExists(HeadBucketRequest + .builder() + .bucket(bucket.name()) + .build() + ) bucketDeleted.matched().exception().get().also { assertThat(it).isNotNull } @@ -387,8 +398,13 @@ internal abstract class S3TestBase { private fun isObjectLockEnabled(bucket: Bucket): Boolean { return try { ObjectLockEnabled.ENABLED == _s3ClientV2.getObjectLockConfiguration( - GetObjectLockConfigurationRequest.builder().bucket(bucket.name()).build() - ).objectLockConfiguration().objectLockEnabled() + GetObjectLockConfigurationRequest + .builder() + .bucket(bucket.name()) + .build() + ) + .objectLockConfiguration() + .objectLockEnabled() } catch (e: S3Exception) { //#getObjectLockConfiguration throws S3Exception if not set false @@ -397,14 +413,20 @@ internal abstract class S3TestBase { private fun deleteMultipartUploads(bucket: Bucket) { _s3ClientV2.listMultipartUploads( - ListMultipartUploadsRequest.builder().bucket(bucket.name()).build() - ).uploads().forEach(Consumer { upload: MultipartUpload -> + ListMultipartUploadsRequest + .builder() + .bucket(bucket.name()) + .build() + ).uploads().forEach { _s3ClientV2.abortMultipartUpload( - AbortMultipartUploadRequest.builder().bucket(bucket.name()).key(upload.key()) - .uploadId(upload.uploadId()).build() + AbortMultipartUploadRequest + .builder() + .bucket(bucket.name()) + .key(it.key()) + .uploadId(it.uploadId()) + .build() ) } - ) } private val s3Endpoint: String? @@ -564,43 +586,47 @@ internal abstract class S3TestBase { else -> throw IllegalArgumentException("Unknown checksum algorithm") } - fun S3Response.checksum(checksumAlgorithm: ChecksumAlgorithm): String { - fun S3Response.checksumSHA1(): String { + fun S3Response.checksum(checksumAlgorithm: ChecksumAlgorithm): String? { + fun S3Response.checksumSHA1(): String? { return when (this) { is GetObjectResponse -> this.checksumSHA1() is PutObjectResponse -> this.checksumSHA1() is HeadObjectResponse -> this.checksumSHA1() is UploadPartResponse -> this.checksumSHA1() + is GetObjectAttributesResponse -> this.checksum().checksumSHA1() else -> throw RuntimeException("Unexpected response type ${this::class.java}") } } - fun S3Response.checksumSHA256(): String { + fun S3Response.checksumSHA256(): String? { return when (this) { is GetObjectResponse -> this.checksumSHA256() is PutObjectResponse -> this.checksumSHA256() is HeadObjectResponse -> this.checksumSHA256() is UploadPartResponse -> this.checksumSHA256() + is GetObjectAttributesResponse -> this.checksum().checksumSHA256() else -> throw RuntimeException("Unexpected response type ${this::class.java}") } } - fun S3Response.checksumCRC32(): String { + fun S3Response.checksumCRC32(): String? { return when (this) { is GetObjectResponse -> this.checksumCRC32() is PutObjectResponse -> this.checksumCRC32() is HeadObjectResponse -> this.checksumCRC32() is UploadPartResponse -> this.checksumCRC32() + is GetObjectAttributesResponse -> this.checksum().checksumCRC32() else -> throw RuntimeException("Unexpected response type ${this::class.java}") } } - fun S3Response.checksumCRC32C(): String { + fun S3Response.checksumCRC32C(): String? { return when (this) { is GetObjectResponse -> this.checksumCRC32C() is PutObjectResponse -> this.checksumCRC32C() is HeadObjectResponse -> this.checksumCRC32C() is UploadPartResponse -> this.checksumCRC32C() + is GetObjectAttributesResponse -> this.checksum().checksumCRC32C() else -> throw RuntimeException("Unexpected response type ${this::class.java}") } } @@ -630,6 +656,7 @@ internal abstract class S3TestBase { const val BUFFER_SIZE = 128 * 1024 private const val THREAD_COUNT = 50 private const val PREFIX = "prefix" + val MAPPER = XmlMapper.builder().build() private val TEST_FILE_NAMES = listOf( SAMPLE_FILE, SAMPLE_FILE_LARGE, @@ -650,6 +677,9 @@ internal abstract class S3TestBase { .filter { it != StorageClass.UNKNOWN_TO_SDK_VERSION } .filter { it != StorageClass.SNOW } .filter { it != StorageClass.EXPRESS_ONEZONE } + .filter { it != StorageClass.GLACIER } + .filter { it != StorageClass.DEEP_ARCHIVE } + .filter { it != StorageClass.OUTPOSTS } .map { it } .stream() } diff --git a/pom.xml b/pom.xml index 7d4032a0f..b4973bedd 100644 --- a/pom.xml +++ b/pom.xml @@ -160,6 +160,11 @@ ${project.version} + + com.amazonaws + aws-java-sdk-core + ${aws.version} + com.amazonaws aws-java-sdk-s3 diff --git a/server/src/main/java/com/adobe/testing/s3mock/BucketController.java b/server/src/main/java/com/adobe/testing/s3mock/BucketController.java index f29b18875..8d826647e 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/BucketController.java +++ b/server/src/main/java/com/adobe/testing/s3mock/BucketController.java @@ -17,6 +17,7 @@ package com.adobe.testing.s3mock; import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_OBJECT_LOCK_ENABLED; +import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_OBJECT_OWNERSHIP; import static com.adobe.testing.s3mock.util.AwsHttpParameters.CONTINUATION_TOKEN; import static com.adobe.testing.s3mock.util.AwsHttpParameters.ENCODING_TYPE; import static com.adobe.testing.s3mock.util.AwsHttpParameters.KEY_MARKER; @@ -57,6 +58,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.model.ObjectOwnership; /** * Handles requests related to buckets. @@ -119,10 +121,12 @@ public ResponseEntity listBuckets() { ) public ResponseEntity createBucket(@PathVariable final String bucketName, @RequestHeader(value = X_AMZ_BUCKET_OBJECT_LOCK_ENABLED, - required = false, defaultValue = "false") boolean objectLockEnabled) { + required = false, defaultValue = "false") boolean objectLockEnabled, + @RequestHeader(value = X_AMZ_OBJECT_OWNERSHIP, + required = false, defaultValue = "BucketOwnerEnforced") ObjectOwnership objectOwnership) { bucketService.verifyBucketNameIsAllowed(bucketName); bucketService.verifyBucketDoesNotExist(bucketName); - bucketService.createBucket(bucketName, objectLockEnabled); + bucketService.createBucket(bucketName, objectLockEnabled, objectOwnership); return ResponseEntity.ok().build(); } diff --git a/server/src/main/java/com/adobe/testing/s3mock/MultipartController.java b/server/src/main/java/com/adobe/testing/s3mock/MultipartController.java index 21ccb4849..fb4279631 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/MultipartController.java +++ b/server/src/main/java/com/adobe/testing/s3mock/MultipartController.java @@ -385,14 +385,14 @@ public ResponseEntity completeMultipartUpload( encryptionHeadersFrom(httpHeaders), locationWithEncodedKey); - String checksum = result.getRight().checksum(); - ChecksumAlgorithm checksumAlgorithm = result.getRight().checksumAlgorithm(); + String checksum = result.checksum(); + ChecksumAlgorithm checksumAlgorithm = result.multipartUploadInfo().checksumAlgorithm(); - //return checksum and encryption headers. + //return encryption headers. //return version id return ResponseEntity .ok() .headers(h -> h.setAll(checksumHeaderFrom(checksum, checksumAlgorithm))) - .body(result.getLeft()); + .body(result); } } diff --git a/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java b/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java index 9111790ee..b0d59333b 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java +++ b/server/src/main/java/com/adobe/testing/s3mock/ObjectController.java @@ -16,6 +16,7 @@ package com.adobe.testing.s3mock; +import static com.adobe.testing.s3mock.dto.StorageClass.STANDARD; import static com.adobe.testing.s3mock.service.ObjectService.getChecksum; import static com.adobe.testing.s3mock.util.AwsHttpHeaders.CONTENT_MD5; import static com.adobe.testing.s3mock.util.AwsHttpHeaders.MetadataDirective.METADATA_DIRECTIVE_COPY; @@ -51,6 +52,7 @@ import static com.adobe.testing.s3mock.util.HeaderUtil.encryptionHeadersFrom; import static com.adobe.testing.s3mock.util.HeaderUtil.mediaTypeFrom; import static com.adobe.testing.s3mock.util.HeaderUtil.overrideHeadersFrom; +import static com.adobe.testing.s3mock.util.HeaderUtil.storageClassHeadersFrom; import static com.adobe.testing.s3mock.util.HeaderUtil.storeHeadersFrom; import static com.adobe.testing.s3mock.util.HeaderUtil.userMetadataFrom; import static com.adobe.testing.s3mock.util.HeaderUtil.userMetadataHeadersFrom; @@ -192,15 +194,15 @@ public ResponseEntity headObject(@PathVariable String bucketName, objectService.verifyObjectMatching(match, noneMatch, s3ObjectMetadata); return ResponseEntity.ok() .eTag(s3ObjectMetadata.etag()) - .header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES) - .headers(headers -> headers.setAll(s3ObjectMetadata.storeHeaders())) - .headers(headers -> headers.setAll(userMetadataHeadersFrom(s3ObjectMetadata))) - .headers(headers -> headers.setAll(s3ObjectMetadata.encryptionHeaders())) - .headers(h -> h.setAll(checksumHeaderFrom(s3ObjectMetadata))) - .header(X_AMZ_STORAGE_CLASS, s3ObjectMetadata.storageClass().toString()) .lastModified(s3ObjectMetadata.lastModified()) .contentLength(Long.parseLong(s3ObjectMetadata.size())) .contentType(mediaTypeFrom(s3ObjectMetadata.contentType())) + .header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES) + .headers(h -> h.setAll(s3ObjectMetadata.storeHeaders())) + .headers(h -> h.setAll(userMetadataHeadersFrom(s3ObjectMetadata))) + .headers(h -> h.setAll(s3ObjectMetadata.encryptionHeaders())) + .headers(h -> h.setAll(checksumHeaderFrom(s3ObjectMetadata))) + .headers(h -> h.setAll(storageClassHeadersFrom(s3ObjectMetadata))) .build(); } else { return ResponseEntity.status(NOT_FOUND).build(); @@ -274,15 +276,15 @@ public ResponseEntity getObject(@PathVariable String buck .ok() .eTag(s3ObjectMetadata.etag()) .header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES) - .headers(headers -> headers.setAll(s3ObjectMetadata.storeHeaders())) - .headers(headers -> headers.setAll(userMetadataHeadersFrom(s3ObjectMetadata))) - .headers(headers -> headers.setAll(s3ObjectMetadata.encryptionHeaders())) - .headers(h -> h.setAll(checksumHeaderFrom(s3ObjectMetadata))) - .header(X_AMZ_STORAGE_CLASS, s3ObjectMetadata.storageClass().toString()) .lastModified(s3ObjectMetadata.lastModified()) .contentLength(Long.parseLong(s3ObjectMetadata.size())) .contentType(mediaTypeFrom(s3ObjectMetadata.contentType())) - .headers(headers -> headers.setAll(overrideHeadersFrom(queryParams))) + .headers(h -> h.setAll(s3ObjectMetadata.storeHeaders())) + .headers(h -> h.setAll(userMetadataHeadersFrom(s3ObjectMetadata))) + .headers(h -> h.setAll(s3ObjectMetadata.encryptionHeaders())) + .headers(h -> h.setAll(checksumHeaderFrom(s3ObjectMetadata))) + .headers(h -> h.setAll(storageClassHeadersFrom(s3ObjectMetadata))) + .headers(h -> h.setAll(overrideHeadersFrom(queryParams))) .body(outputStream -> Files.copy(s3ObjectMetadata.dataPath(), outputStream)); } @@ -545,17 +547,25 @@ public ResponseEntity getObjectAttributes( S3ObjectMetadata s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.key()); objectService.verifyObjectMatching(match, noneMatch, s3ObjectMetadata); + //S3Mock stores the etag with the additional quotation marks needed in the headers. This + // response does not use eTag as a header, so it must not contain the quotation marks. + String etag = s3ObjectMetadata.etag().replace("\"", ""); + long objectSize = Long.parseLong(s3ObjectMetadata.size()); + //in object attributes, S3 returns STANDARD, in all other APIs it returns null... + StorageClass storageClass = s3ObjectMetadata.storageClass() == null + ? STANDARD + : s3ObjectMetadata.storageClass(); GetObjectAttributesOutput response = new GetObjectAttributesOutput( getChecksum(s3ObjectMetadata), objectAttributes.contains(ObjectAttributes.ETAG.toString()) - ? s3ObjectMetadata.etag() + ? etag : null, null, //parts not supported right now objectAttributes.contains(ObjectAttributes.OBJECT_SIZE.toString()) - ? Long.parseLong(s3ObjectMetadata.size()) + ? objectSize : null, objectAttributes.contains(ObjectAttributes.STORAGE_CLASS.toString()) - ? s3ObjectMetadata.storageClass() + ? storageClass : null ); @@ -682,6 +692,7 @@ public ResponseEntity copyObject(@PathVariable String bucketNa @RequestHeader(value = X_AMZ_COPY_SOURCE_IF_MATCH, required = false) List match, @RequestHeader(value = X_AMZ_COPY_SOURCE_IF_NONE_MATCH, required = false) List noneMatch, + @RequestHeader(value = X_AMZ_STORAGE_CLASS, required = false) StorageClass storageClass, @RequestHeader HttpHeaders httpHeaders) { //TODO: needs modified-since handling, see API @@ -694,17 +705,13 @@ public ResponseEntity copyObject(@PathVariable String bucketNa metadata = userMetadataFrom(httpHeaders); } - //TODO: this is potentially illegal on S3. S3 throws a 400: - // "This copy request is illegal because it is trying to copy an object to itself without - // changing the object's metadata, storage class, website redirect location or encryption - // attributes." - var copyObjectResult = objectService.copyS3Object(copySource.bucket(), copySource.key(), bucketName, key.key(), encryptionHeadersFrom(httpHeaders), - metadata); + metadata, + storageClass); //return version id / copy source version id //return expiration diff --git a/server/src/main/java/com/adobe/testing/s3mock/ObjectOwnershipHeaderConverter.java b/server/src/main/java/com/adobe/testing/s3mock/ObjectOwnershipHeaderConverter.java new file mode 100644 index 000000000..c660f8cf4 --- /dev/null +++ b/server/src/main/java/com/adobe/testing/s3mock/ObjectOwnershipHeaderConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2024 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; + +import com.adobe.testing.s3mock.util.AwsHttpHeaders; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import software.amazon.awssdk.services.s3.model.ObjectOwnership; + +/** + * Converts values of the {@link AwsHttpHeaders#X_AMZ_OBJECT_OWNERSHIP} which is sent by the Amazon + * client. + * Example: x-amz-object-ownership: ObjectWriter + * API Reference + * API Reference + * API Reference + */ +class ObjectOwnershipHeaderConverter implements Converter { + + @Override + @Nullable + public ObjectOwnership convert(@NonNull String source) { + return ObjectOwnership.fromValue(source); + } +} diff --git a/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java b/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java index 39d7f84bf..aac596785 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java +++ b/server/src/main/java/com/adobe/testing/s3mock/S3Exception.java @@ -30,7 +30,8 @@ * API Reference */ public class S3Exception extends RuntimeException { - private static final String INVALID_REQUEST = "InvalidRequest"; + private static final String INVALID_REQUEST_CODE = "InvalidRequest"; + private static final String BAD_REQUEST_CODE = "BadRequest"; public static final S3Exception INVALID_PART_NUMBER = new S3Exception(BAD_REQUEST.value(), "InvalidArgument", "Part number must be an integer between 1 and 10000, inclusive"); @@ -78,23 +79,28 @@ public class S3Exception extends RuntimeException { new S3Exception(CONFLICT.value(), "BucketAlreadyOwnedByYou", "Your previous request to create the named bucket succeeded and you already own it."); public static final S3Exception NOT_FOUND_BUCKET_OBJECT_LOCK = - new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST, + new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE, "Bucket is missing Object Lock Configuration"); public static final S3Exception NOT_FOUND_OBJECT_LOCK = new S3Exception(NOT_FOUND.value(), "NotFound", "The specified object does not have a ObjectLock configuration"); public static final S3Exception INVALID_REQUEST_RETAINDATE = - new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST, + new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE, "The retain until date must be in the future!"); public static final S3Exception INVALID_REQUEST_MAXKEYS = - new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST, + new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE, "maxKeys should be non-negative"); public static final S3Exception INVALID_REQUEST_ENCODINGTYPE = - new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST, + new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE, "encodingtype can only be none or 'url'"); + public static final S3Exception INVALID_COPY_REQUEST_SAME_KEY = + new S3Exception(BAD_REQUEST.value(), INVALID_REQUEST_CODE, + "This copy request is illegal because it is trying to copy an object to itself without " + + "changing the object's metadata, storage class, website redirect location or " + + "encryption attributes."); public static final S3Exception BAD_REQUEST_MD5 = - new S3Exception(BAD_REQUEST.value(), "BadRequest", + new S3Exception(BAD_REQUEST.value(), BAD_REQUEST_CODE, "Content-MD5 does not match object md5"); public static final S3Exception BAD_REQUEST_CONTENT = new S3Exception(BAD_REQUEST.value(), "UnexpectedContent", @@ -103,6 +109,18 @@ public class S3Exception extends RuntimeException { new S3Exception(BAD_REQUEST.value(), "BadDigest", "The Content-MD5 or checksum value that you specified did " + "not match what the server received."); + public static final S3Exception BAD_CHECKSUM_SHA1 = + new S3Exception(BAD_REQUEST.value(), BAD_REQUEST_CODE, + "Value for x-amz-checksum-sha1 header is invalid."); + public static final S3Exception BAD_CHECKSUM_SHA256 = + new S3Exception(BAD_REQUEST.value(), BAD_REQUEST_CODE, + "Value for x-amz-checksum-sha256 header is invalid."); + public static final S3Exception BAD_CHECKSUM_CRC32 = + new S3Exception(BAD_REQUEST.value(), BAD_REQUEST_CODE, + "Value for x-amz-checksum-crc32 header is invalid."); + public static final S3Exception BAD_CHECKSUM_CRC32C = + new S3Exception(BAD_REQUEST.value(), BAD_REQUEST_CODE, + "Value for x-amz-checksum-crc32c header is invalid."); private final int status; private final String code; private final String message; diff --git a/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java b/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java index 954312ae9..aa1a5529b 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java +++ b/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java @@ -211,6 +211,11 @@ HttpRangeHeaderConverter httpRangeHeaderConverter() { return new HttpRangeHeaderConverter(); } + @Bean + ObjectOwnershipHeaderConverter objectOwnershipHeaderConverter() { + return new ObjectOwnershipHeaderConverter(); + } + /** * {@link ResponseEntityExceptionHandler} dealing with {@link S3Exception}s; Serializes them to * response output as suitable ErrorResponses. diff --git a/server/src/main/java/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResult.java b/server/src/main/java/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResult.java index 5f636f883..59b30a6c5 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResult.java +++ b/server/src/main/java/com/adobe/testing/s3mock/dto/CompleteMultipartUploadResult.java @@ -18,6 +18,8 @@ import static com.adobe.testing.s3mock.util.EtagUtil.normalizeEtag; +import com.adobe.testing.s3mock.store.MultipartUploadInfo; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; @@ -38,7 +40,11 @@ public record CompleteMultipartUploadResult( String etag, //workaround for adding xmlns attribute to root element only. @JacksonXmlProperty(isAttribute = true, localName = "xmlns") - String xmlns + String xmlns, + + @JsonIgnore + MultipartUploadInfo multipartUploadInfo, + String checksum ) { public CompleteMultipartUploadResult { etag = normalizeEtag(etag); @@ -47,7 +53,12 @@ public record CompleteMultipartUploadResult( } } - public CompleteMultipartUploadResult(String location, String bucket, String key, String etag) { - this(location, bucket, key, etag, null); + public CompleteMultipartUploadResult(String location, + String bucket, + String key, + String etag, + MultipartUploadInfo multipartUploadInfo, + String checksum) { + this(location, bucket, key, etag, null, multipartUploadInfo, checksum); } } diff --git a/server/src/main/java/com/adobe/testing/s3mock/dto/GetObjectAttributesOutput.java b/server/src/main/java/com/adobe/testing/s3mock/dto/GetObjectAttributesOutput.java index 2eb44411d..5e3c146b4 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/dto/GetObjectAttributesOutput.java +++ b/server/src/main/java/com/adobe/testing/s3mock/dto/GetObjectAttributesOutput.java @@ -44,7 +44,6 @@ public record GetObjectAttributesOutput( ) { public GetObjectAttributesOutput { - etag = normalizeEtag(etag); if (xmlns == null) { xmlns = "http://s3.amazonaws.com/doc/2006-03-01/"; } diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java b/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java index 64f89dc15..d8169ea88 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java +++ b/server/src/main/java/com/adobe/testing/s3mock/service/BucketService.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 Adobe. + * Copyright 2017-2024 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.UnaryOperator; +import software.amazon.awssdk.services.s3.model.ObjectOwnership; import software.amazon.awssdk.utils.http.SdkHttpUtils; public class BucketService { @@ -98,8 +99,14 @@ public Bucket getBucket(String bucketName) { * * @return the Bucket */ - public Bucket createBucket(String bucketName, boolean objectLockEnabled) { - return Bucket.from(bucketStore.createBucket(bucketName, objectLockEnabled)); + public Bucket createBucket(String bucketName, + boolean objectLockEnabled, + ObjectOwnership objectOwnership) { + return Bucket.from( + bucketStore.createBucket(bucketName, + objectLockEnabled, + objectOwnership) + ); } public boolean deleteBucket(String bucketName) { diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/MultipartService.java b/server/src/main/java/com/adobe/testing/s3mock/service/MultipartService.java index bbff96f47..d05c741a3 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/service/MultipartService.java +++ b/server/src/main/java/com/adobe/testing/s3mock/service/MultipartService.java @@ -35,7 +35,6 @@ import com.adobe.testing.s3mock.dto.StorageClass; import com.adobe.testing.s3mock.store.BucketStore; import com.adobe.testing.s3mock.store.MultipartStore; -import com.adobe.testing.s3mock.store.MultipartUploadInfo; import java.nio.file.Path; import java.util.Collections; import java.util.Date; @@ -43,7 +42,6 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; -import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpRange; @@ -177,7 +175,7 @@ public void abortMultipartUpload(String bucketName, String key, String uploadId) * * @return etag of the uploaded file. */ - public Pair completeMultipartUpload( + public CompleteMultipartUploadResult completeMultipartUpload( String bucketName, String key, String uploadId, @@ -190,10 +188,9 @@ public Pair completeMultipar return null; } var multipartUploadInfo = multipartStore.getMultipartUploadInfo(bucketMetadata, uploadId); - var etag = multipartStore - .completeMultipartUpload(bucketMetadata, key, id, uploadId, parts, encryptionHeaders); - return Pair.of(new CompleteMultipartUploadResult(location, bucketName, key, etag), - multipartUploadInfo); + return multipartStore + .completeMultipartUpload(bucketMetadata, key, id, uploadId, parts, encryptionHeaders, + multipartUploadInfo, location); } /** @@ -221,7 +218,6 @@ public InitiateMultipartUploadResult createMultipartUpload(String bucketName, String checksum, ChecksumAlgorithm checksumAlgorithm) { var bucketMetadata = bucketStore.getBucketMetadata(bucketName); - //TODO: add upload to bucket var id = bucketStore.addKeyToBucket(key, bucketName); try { @@ -288,7 +284,6 @@ public void verifyMultipartParts(String bucketName, String key, var bucketMetadata = bucketStore.getBucketMetadata(bucketName); var id = bucketMetadata.getID(key); if (id == null) { - //TODO: is this the correct error? throw INVALID_PART; } verifyMultipartParts(bucketName, id, uploadId); diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java index 3a9e1c912..90541f174 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java +++ b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java @@ -16,7 +16,6 @@ package com.adobe.testing.s3mock.service; -import static com.adobe.testing.s3mock.S3Exception.BAD_DIGEST; import static com.adobe.testing.s3mock.S3Exception.BAD_REQUEST_CONTENT; import static com.adobe.testing.s3mock.S3Exception.BAD_REQUEST_MD5; import static com.adobe.testing.s3mock.S3Exception.INVALID_REQUEST_RETAINDATE; @@ -81,7 +80,8 @@ public CopyObjectResult copyS3Object(String sourceBucketName, String destinationBucketName, String destinationKey, Map encryptionHeaders, - Map userMetadata) { + Map userMetadata, + StorageClass storageClass) { var sourceBucketMetadata = bucketStore.getBucketMetadata(sourceBucketName); var destinationBucketMetadata = bucketStore.getBucketMetadata(destinationBucketName); var sourceId = sourceBucketMetadata.getID(sourceKey); @@ -91,7 +91,11 @@ public CopyObjectResult copyS3Object(String sourceBucketName, // source and destination is the same, pretend we copied - S3 does the same. if (sourceKey.equals(destinationKey) && sourceBucketName.equals(destinationBucketName)) { - return objectStore.pretendToCopyS3Object(sourceBucketMetadata, sourceId, userMetadata); + return objectStore.pretendToCopyS3Object(sourceBucketMetadata, + sourceId, + userMetadata, + encryptionHeaders, + storageClass); } // source must be copied to destination @@ -99,7 +103,7 @@ public CopyObjectResult copyS3Object(String sourceBucketName, try { return objectStore.copyS3Object(sourceBucketMetadata, sourceId, destinationBucketMetadata, destinationId, destinationKey, - encryptionHeaders, userMetadata); + encryptionHeaders, userMetadata, storageClass); } catch (Exception e) { //something went wrong with writing the destination file, clean up ID from BucketStore. bucketStore.removeFromBucket(destinationKey, destinationBucketName); @@ -255,13 +259,6 @@ public void verifyRetention(Retention retention) { } } - public void verifyChecksum(Path path, String checksum, ChecksumAlgorithm checksumAlgorithm) { - String checksumFor = DigestUtil.checksumFor(path, checksumAlgorithm.toAlgorithm()); - if (!checksum.equals(checksumFor)) { - throw BAD_DIGEST; - } - } - public void verifyMd5(Path input, String contentMd5) { try { try (var stream = Files.newInputStream(input)) { diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/ServiceBase.java b/server/src/main/java/com/adobe/testing/s3mock/service/ServiceBase.java index c0cd35c02..dd6154f51 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/service/ServiceBase.java +++ b/server/src/main/java/com/adobe/testing/s3mock/service/ServiceBase.java @@ -16,6 +16,10 @@ package com.adobe.testing.s3mock.service; +import static com.adobe.testing.s3mock.S3Exception.BAD_CHECKSUM_CRC32; +import static com.adobe.testing.s3mock.S3Exception.BAD_CHECKSUM_CRC32C; +import static com.adobe.testing.s3mock.S3Exception.BAD_CHECKSUM_SHA1; +import static com.adobe.testing.s3mock.S3Exception.BAD_CHECKSUM_SHA256; import static com.adobe.testing.s3mock.S3Exception.BAD_DIGEST; import static com.adobe.testing.s3mock.S3Exception.BAD_REQUEST_CONTENT; import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_DECODED_CONTENT_LENGTH; @@ -39,10 +43,14 @@ abstract class ServiceBase { public void verifyChecksum(Path path, String checksum, ChecksumAlgorithm checksumAlgorithm) { - if (checksum != null && checksumAlgorithm != null) { - String checksumFor = DigestUtil.checksumFor(path, checksumAlgorithm.toAlgorithm()); - if (!checksum.equals(checksumFor)) { - throw BAD_DIGEST; + String checksumFor = DigestUtil.checksumFor(path, checksumAlgorithm.toAlgorithm()); + if (!checksum.equals(checksumFor)) { + switch (checksumAlgorithm) { + case SHA1 -> throw BAD_CHECKSUM_SHA1; + case SHA256 -> throw BAD_CHECKSUM_SHA256; + case CRC32 -> throw BAD_CHECKSUM_CRC32; + case CRC32C -> throw BAD_CHECKSUM_CRC32C; + default -> throw BAD_DIGEST; } } } diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java b/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java index 213b233b5..b0b984bc6 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/BucketMetadata.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +import software.amazon.awssdk.services.s3.model.ObjectOwnership; /** * Represents a bucket in S3, used to serialize and deserialize all metadata locally. @@ -31,6 +32,7 @@ public record BucketMetadata( String creationDate, ObjectLockConfiguration objectLockConfiguration, BucketLifecycleConfiguration bucketLifecycleConfiguration, + ObjectOwnership objectOwnership, Path path, Map objects ) { @@ -38,11 +40,13 @@ public record BucketMetadata( public BucketMetadata(String name, String creationDate, ObjectLockConfiguration objectLockConfiguration, BucketLifecycleConfiguration bucketLifecycleConfiguration, + ObjectOwnership objectOwnership, Path path) { this(name, creationDate, objectLockConfiguration, bucketLifecycleConfiguration, + objectOwnership, path, new HashMap<>()); } @@ -50,13 +54,17 @@ public BucketMetadata(String name, String creationDate, public BucketMetadata withObjectLockConfiguration( ObjectLockConfiguration objectLockConfiguration) { return new BucketMetadata(name(), creationDate(), objectLockConfiguration, - bucketLifecycleConfiguration(), path()); + bucketLifecycleConfiguration(), + objectOwnership(), + path()); } public BucketMetadata withBucketLifecycleConfiguration( BucketLifecycleConfiguration bucketLifecycleConfiguration) { return new BucketMetadata(name(), creationDate(), objectLockConfiguration(), - bucketLifecycleConfiguration, path()); + bucketLifecycleConfiguration, + objectOwnership(), + path()); } public boolean doesKeyExist(String key) { diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java b/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java index 6c446d63a..fe75784a4 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/BucketStore.java @@ -35,6 +35,7 @@ import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.s3.model.ObjectOwnership; /** * Stores buckets and their metadata created in S3Mock. @@ -178,7 +179,9 @@ private List findBucketPaths() { * @throws IllegalStateException if the bucket cannot be created or the bucket already exists but * is not a directory. */ - public BucketMetadata createBucket(String bucketName, boolean objectLockEnabled) { + public BucketMetadata createBucket(String bucketName, + boolean objectLockEnabled, + ObjectOwnership objectOwnership) { var bucketMetadata = getBucketMetadata(bucketName); if (bucketMetadata != null) { throw new IllegalStateException("Bucket already exists."); @@ -193,6 +196,7 @@ public BucketMetadata createBucket(String bucketName, boolean objectLockEnabled) objectLockEnabled ? new ObjectLockConfiguration(ObjectLockEnabled.ENABLED, null) : null, null, + objectOwnership, bucketFolder.toPath() ); writeToDisk(newBucketMetadata); diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/MultipartStore.java b/server/src/main/java/com/adobe/testing/s3mock/store/MultipartStore.java index 98eb33cfc..098a37ed9 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/MultipartStore.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/MultipartStore.java @@ -25,6 +25,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import com.adobe.testing.s3mock.dto.ChecksumAlgorithm; +import com.adobe.testing.s3mock.dto.CompleteMultipartUploadResult; import com.adobe.testing.s3mock.dto.CompletedPart; import com.adobe.testing.s3mock.dto.MultipartUpload; import com.adobe.testing.s3mock.dto.Owner; @@ -234,9 +235,14 @@ public String putPart(BucketMetadata bucket, * * @return etag of the uploaded file. */ - public String completeMultipartUpload(BucketMetadata bucket, String key, UUID id, - String uploadId, List parts, Map encryptionHeaders) { - var uploadInfo = getMultipartUploadInfo(bucket, uploadId); + public CompleteMultipartUploadResult completeMultipartUpload(BucketMetadata bucket, + String key, + UUID id, + String uploadId, + List parts, + Map encryptionHeaders, + MultipartUploadInfo uploadInfo, + String location) { if (uploadInfo == null) { throw new IllegalArgumentException("Unknown upload " + uploadId); } @@ -252,7 +258,7 @@ public String completeMultipartUpload(BucketMetadata bucket, String key, UUID id try (var inputStream = toInputStream(partsPaths)) { tempFile = Files.createTempFile("completeMultipartUpload", ""); inputStream.transferTo(Files.newOutputStream(tempFile)); - var checksumFor = checksumFor(tempFile, uploadInfo); + var checksumFor = checksumFor(partsPaths, uploadInfo); var etag = hexDigestMultipart(partsPaths); objectStore.storeS3ObjectMetadata(bucket, id, @@ -270,7 +276,8 @@ public String completeMultipartUpload(BucketMetadata bucket, String key, UUID id uploadInfo.storageClass() ); FileUtils.deleteDirectory(partFolder.toFile()); - return etag; + return new CompleteMultipartUploadResult(location, uploadInfo.bucket(), + key, etag, uploadInfo, checksumFor); } catch (IOException e) { throw new IllegalStateException(String.format( "Error finishing multipart upload bucket=%s, key=%s, id=%s, uploadId=%s", @@ -282,9 +289,9 @@ public String completeMultipartUpload(BucketMetadata bucket, String key, UUID id } } - private String checksumFor(Path path, MultipartUploadInfo uploadInfo) { + private String checksumFor(List paths, MultipartUploadInfo uploadInfo) { if (uploadInfo.checksumAlgorithm() != null) { - return DigestUtil.checksumFor(path, uploadInfo.checksumAlgorithm().toAlgorithm()); + return DigestUtil.checksumMultipart(paths, uploadInfo.checksumAlgorithm().toAlgorithm()); } return null; } @@ -463,7 +470,7 @@ private Path getUploadMetadataPath(BucketMetadata bucket, String uploadId) { return getPartsFolder(bucket, uploadId).resolve(MULTIPART_UPLOAD_META_FILE); } - public MultipartUploadInfo getUploadMetadata(BucketMetadata bucket, String uploadId) { + private MultipartUploadInfo getUploadMetadata(BucketMetadata bucket, String uploadId) { var metaPath = getUploadMetadataPath(bucket, uploadId); if (Files.exists(metaPath)) { diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java b/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java index 25925c05e..9cc2999a7 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/ObjectStore.java @@ -16,6 +16,7 @@ package com.adobe.testing.s3mock.store; +import static com.adobe.testing.s3mock.S3Exception.INVALID_COPY_REQUEST_SAME_KEY; import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID; import static com.adobe.testing.s3mock.util.DigestUtil.hexDigest; @@ -53,7 +54,7 @@ public class ObjectStore extends StoreBase { private static final Logger LOG = LoggerFactory.getLogger(ObjectStore.class); private static final String META_FILE = "objectMetadata.json"; - private static final String ACL_FILE = "objectAcl.xml"; + private static final String ACL_FILE = "objectAcl.json"; private static final String DATA_FILE = "binaryData"; /** @@ -304,7 +305,8 @@ public CopyObjectResult copyS3Object(BucketMetadata sourceBucket, UUID destinationId, String destinationKey, Map encryptionHeaders, - Map userMetadata) { + Map userMetadata, + StorageClass storageClass) { var sourceObject = getS3ObjectMetadata(sourceBucket, sourceId); if (sourceObject == null) { return null; @@ -318,13 +320,14 @@ public CopyObjectResult copyS3Object(BucketMetadata sourceBucket, sourceObject.dataPath(), userMetadata == null || userMetadata.isEmpty() ? sourceObject.userMetadata() : userMetadata, - encryptionHeaders, + encryptionHeaders == null || encryptionHeaders.isEmpty() + ? sourceObject.encryptionHeaders() : encryptionHeaders, null, sourceObject.tags(), sourceObject.checksumAlgorithm(), sourceObject.checksum(), sourceObject.owner(), - sourceObject.storageClass() + storageClass != null ? storageClass : sourceObject.storageClass() ); return new CopyObjectResult(copiedObject.modificationDate(), copiedObject.etag()); } @@ -337,12 +340,16 @@ public CopyObjectResult copyS3Object(BucketMetadata sourceBucket, */ public CopyObjectResult pretendToCopyS3Object(BucketMetadata sourceBucket, UUID sourceId, - Map userMetadata) { + Map userMetadata, + Map encryptionHeaders, + StorageClass storageClass) { var sourceObject = getS3ObjectMetadata(sourceBucket, sourceId); if (sourceObject == null) { return null; } + verifyPretendCopy(sourceObject, userMetadata, encryptionHeaders, storageClass); + writeMetafile(sourceBucket, new S3ObjectMetadata( sourceObject.id(), sourceObject.key(), @@ -359,14 +366,27 @@ public CopyObjectResult pretendToCopyS3Object(BucketMetadata sourceBucket, sourceObject.retention(), sourceObject.owner(), sourceObject.storeHeaders(), - sourceObject.encryptionHeaders(), + encryptionHeaders == null || encryptionHeaders.isEmpty() + ? sourceObject.encryptionHeaders() : encryptionHeaders, sourceObject.checksumAlgorithm(), sourceObject.checksum(), - sourceObject.storageClass() + storageClass != null ? storageClass : sourceObject.storageClass() )); return new CopyObjectResult(sourceObject.modificationDate(), sourceObject.etag()); } + private void verifyPretendCopy(S3ObjectMetadata sourceObject, + Map userMetadata, + Map encryptionHeaders, + StorageClass storageClass) { + var userDataUnChanged = userMetadata == null || userMetadata.isEmpty(); + var encryptionHeadersUnChanged = encryptionHeaders == null || encryptionHeaders.isEmpty(); + var storageClassUnChanged = storageClass == null || storageClass == sourceObject.storageClass(); + if (userDataUnChanged && storageClassUnChanged && encryptionHeadersUnChanged) { + throw INVALID_COPY_REQUEST_SAME_KEY; + } + } + /** * Removes an object key from a bucket. * diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java b/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java index 7a4ce168c..a68f655eb 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/S3ObjectMetadata.java @@ -64,6 +64,6 @@ public record S3ObjectMetadata( tags = Objects.requireNonNullElse(tags, new ArrayList<>()); storeHeaders = storeHeaders == null ? Collections.emptyMap() : storeHeaders; encryptionHeaders = encryptionHeaders == null ? Collections.emptyMap() : encryptionHeaders; - storageClass = storageClass == null ? StorageClass.STANDARD : storageClass; + storageClass = storageClass == StorageClass.STANDARD ? null : storageClass; } } diff --git a/server/src/main/java/com/adobe/testing/s3mock/store/StoreConfiguration.java b/server/src/main/java/com/adobe/testing/s3mock/store/StoreConfiguration.java index 5ec64586f..dbd37191c 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/store/StoreConfiguration.java +++ b/server/src/main/java/com/adobe/testing/s3mock/store/StoreConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.services.s3.model.ObjectOwnership; @Configuration @EnableConfigurationProperties(StoreProperties.class) @@ -76,7 +77,10 @@ BucketStore bucketStore(StoreProperties properties, File rootFolder, List { - bucketStore.createBucket(name, false); + bucketStore.createBucket(name, + false, + ObjectOwnership.BUCKET_OWNER_ENFORCED + ); LOG.info("Creating initial bucket {}.", name); }); diff --git a/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpHeaders.java b/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpHeaders.java index 306eea987..744336e50 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpHeaders.java +++ b/server/src/main/java/com/adobe/testing/s3mock/util/AwsHttpHeaders.java @@ -53,6 +53,7 @@ public final class AwsHttpHeaders { public static final String X_AMZ_DELETE_MARKER = "x-amz-delete-marker"; public static final String X_AMZ_BUCKET_OBJECT_LOCK_ENABLED = "x-amz-bucket-object-lock-enabled"; + public static final String X_AMZ_OBJECT_OWNERSHIP = "x-amz-object-ownership"; public static final String X_AMZ_OBJECT_ATTRIBUTES = "x-amz-object-attributes"; public static final String X_AMZ_CHECKSUM_ALGORITHM = "x-amz-checksum-algorithm"; public static final String X_AMZ_SDK_CHECKSUM_ALGORITHM = "x-amz-sdk-checksum-algorithm"; diff --git a/server/src/main/java/com/adobe/testing/s3mock/util/DigestUtil.java b/server/src/main/java/com/adobe/testing/s3mock/util/DigestUtil.java index 9cdf368be..6dd99f65c 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/util/DigestUtil.java +++ b/server/src/main/java/com/adobe/testing/s3mock/util/DigestUtil.java @@ -73,6 +73,17 @@ public static String checksumFor(Path path, Algorithm algorithm) { * @return the checksum */ public static String checksumFor(InputStream is, Algorithm algorithm) { + return BinaryUtils.toBase64(checksum(is, algorithm)); + } + + /** + * Calculate a checksum for the given inputstream and algorithm. + * + * @param is InputStream containing the bytes to generate the checksum for + * @param algorithm algorithm to use + * @return the checksum + */ + public static byte[] checksum(InputStream is, Algorithm algorithm) { SdkChecksum sdkChecksum = SdkChecksum.forAlgorithm(algorithm); try { byte[] buffer = new byte[4096]; @@ -80,16 +91,32 @@ public static String checksumFor(InputStream is, Algorithm algorithm) { while ((read = is.read(buffer)) != -1) { sdkChecksum.update(buffer, 0, read); } - return BinaryUtils.toBase64(sdkChecksum.getChecksumBytes()); + return sdkChecksum.getChecksumBytes(); } catch (IOException e) { throw new IllegalStateException(CHECKSUM_COULD_NOT_BE_CALCULATED, e); } } + private static byte[] checksum(List paths, Algorithm algorithm) { + SdkChecksum sdkChecksum = SdkChecksum.forAlgorithm(algorithm); + var allChecksums = new byte[0]; + for (var path : paths) { + try (var inputStream = Files.newInputStream(path)) { + allChecksums = ArrayUtils.addAll(allChecksums, checksum(inputStream, algorithm)); + } catch (IOException e) { + throw new IllegalStateException("Could not read from path " + path, e); + } + } + sdkChecksum.update(allChecksums, 0, allChecksums.length); + allChecksums = sdkChecksum.getChecksumBytes(); + return allChecksums; + } + /** * Calculates a hex encoded MD5 digest for the contents of a list of paths. * This is a special case that emulates how AWS calculates the MD5 Checksums of the parts of a - * Multipart upload: + * Multipart upload. + * API * * Stackoverflow * @@ -110,6 +137,17 @@ public static String hexDigestMultipart(List paths) { return DigestUtils.md5Hex(md5(null, paths)) + "-" + paths.size(); } + /** + * Calculates the checksum for a list of paths. + * For multipart uploads, AWS takes the checksum of all parts, concatenates them, and then takes + * the checksum again. Then, they add a hyphen and the number of parts used to calculate the + * checksum. + * API + */ + public static String checksumMultipart(List paths, Algorithm algorithm) { + return BinaryUtils.toBase64(checksum(paths, algorithm)) + "-" + paths.size(); + } + public static String hexDigest(byte[] bytes) { return DigestUtils.md5Hex(bytes); } diff --git a/server/src/main/java/com/adobe/testing/s3mock/util/HeaderUtil.java b/server/src/main/java/com/adobe/testing/s3mock/util/HeaderUtil.java index c7861feaa..eb1fe855f 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/util/HeaderUtil.java +++ b/server/src/main/java/com/adobe/testing/s3mock/util/HeaderUtil.java @@ -25,11 +25,13 @@ import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CONTENT_SHA256; import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_SDK_CHECKSUM_ALGORITHM; import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION; +import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_STORAGE_CLASS; import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase; import com.adobe.testing.s3mock.dto.ChecksumAlgorithm; +import com.adobe.testing.s3mock.dto.StorageClass; import com.adobe.testing.s3mock.store.S3ObjectMetadata; import java.util.AbstractMap.SimpleEntry; import java.util.HashMap; @@ -80,6 +82,19 @@ public static Map userMetadataHeadersFrom(S3ObjectMetadata s3Obj return metadataHeaders; } + /** + * Creates response headers from S3ObjectMetadata storageclass. + * @param s3ObjectMetadata {@link S3ObjectMetadata} S3Object where data will be extracted + */ + public static Map storageClassHeadersFrom(S3ObjectMetadata s3ObjectMetadata) { + Map headers = new HashMap<>(); + StorageClass storageClass = s3ObjectMetadata.storageClass(); + if (storageClass != null) { + headers.put(X_AMZ_STORAGE_CLASS, storageClass.toString()); + } + return headers; + } + /** * Retrieves user metadata from request. * @param headers {@link HttpHeaders} diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/BucketControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/BucketControllerTest.kt index 40534cdfa..f576eed9f 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/BucketControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/BucketControllerTest.kt @@ -56,6 +56,7 @@ import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.web.util.UriComponentsBuilder +import software.amazon.awssdk.services.s3.model.ObjectOwnership.BUCKET_OWNER_ENFORCED import java.nio.file.Paths import java.time.Instant @@ -166,7 +167,7 @@ internal class BucketControllerTest : BaseControllerTest() { @Test fun testCreateBucket_InternalServerError() { - whenever(bucketService.createBucket(TEST_BUCKET_NAME, false)) + whenever(bucketService.createBucket(TEST_BUCKET_NAME, false, BUCKET_OWNER_ENFORCED)) .thenThrow(IllegalStateException("THIS IS EXPECTED")) val headers = HttpHeaders().apply { diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/ObjectControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/ObjectControllerTest.kt index 28bb65bf8..98f15b34c 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/ObjectControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/ObjectControllerTest.kt @@ -635,7 +635,7 @@ internal class ObjectControllerTest : BaseControllerTest() { encryptionHeaders(encryption, encryptionKey), null, null, - StorageClass.STANDARD + null ) } 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 dfc571055..75762ae8a 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 @@ -24,7 +24,7 @@ internal class CompleteMultipartUploadResultTest { @Test @Throws(IOException::class) fun testSerialization(testInfo: TestInfo) { - val iut = CompleteMultipartUploadResult("location", "bucket", "key", "etag") + val iut = CompleteMultipartUploadResult("location", "bucket", "key", "etag", null, null) assertThat(iut).isNotNull() DtoTestUtil.serializeAndAssert(iut, testInfo) } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt index 8923ef09c..8147432b2 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/service/MultipartServiceTest.kt @@ -158,7 +158,7 @@ internal class MultipartServiceTest : ServiceTestBase() { val uploadId = "uploadId" val bucketName = "bucketName" whenever(bucketStore.getBucketMetadata(bucketName)) - .thenReturn(BucketMetadata(null, null, null, null, null)) + .thenReturn(BucketMetadata(null, null, null, null, null, null)) whenever( multipartStore.getMultipartUpload( ArgumentMatchers.any( 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 d45a5b9a9..6623c232a 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 @@ -26,6 +26,7 @@ import com.adobe.testing.s3mock.store.ObjectStore import com.adobe.testing.s3mock.store.S3ObjectMetadata import org.mockito.kotlin.whenever import org.springframework.boot.test.mock.mockito.MockBean +import software.amazon.awssdk.services.s3.model.ObjectOwnership.BUCKET_OWNER_ENFORCED import java.nio.file.Files import java.util.Date import java.util.UUID @@ -128,6 +129,7 @@ internal abstract class ServiceTestBase { Date().toString(), null, null, + BUCKET_OWNER_ENFORCED, Files.createTempDirectory(bucketName) ) } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/BucketStoreTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/BucketStoreTest.kt index 01504b296..2d9a1a64c 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/BucketStoreTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/BucketStoreTest.kt @@ -29,6 +29,8 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.mock.mockito.MockBean +import software.amazon.awssdk.services.s3.model.ObjectOwnership +import software.amazon.awssdk.services.s3.model.ObjectOwnership.BUCKET_OWNER_ENFORCED @AutoConfigureWebMvc @AutoConfigureMockMvc @@ -40,14 +42,15 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateBucket() { - val bucket = bucketStore.createBucket(TEST_BUCKET_NAME, false) + val bucket = bucketStore.createBucket(TEST_BUCKET_NAME, false, + BUCKET_OWNER_ENFORCED) assertThat(bucket.name).endsWith(TEST_BUCKET_NAME) assertThat(bucket.path).exists() } @Test fun testDoesBucketExist_ok() { - bucketStore.createBucket(TEST_BUCKET_NAME, false) + bucketStore.createBucket(TEST_BUCKET_NAME, false, BUCKET_OWNER_ENFORCED) val doesBucketExist = bucketStore.doesBucketExist(TEST_BUCKET_NAME) @@ -67,9 +70,9 @@ internal class BucketStoreTest : StoreTestBase() { val bucketName2 = "myNüwNämeZwöei" val bucketName3 = "myNüwNämeDrü" - bucketStore.createBucket(bucketName1, false) - bucketStore.createBucket(bucketName2, false) - bucketStore.createBucket(bucketName3, false) + bucketStore.createBucket(bucketName1, false, BUCKET_OWNER_ENFORCED) + bucketStore.createBucket(bucketName2, false, BUCKET_OWNER_ENFORCED) + bucketStore.createBucket(bucketName3, false, BUCKET_OWNER_ENFORCED) val buckets = bucketStore.listBuckets() @@ -78,7 +81,7 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateAndGetBucket() { - bucketStore.createBucket(TEST_BUCKET_NAME, false) + bucketStore.createBucket(TEST_BUCKET_NAME, false, BUCKET_OWNER_ENFORCED) val bucket = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) assertThat(bucket).isNotNull() @@ -87,7 +90,7 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateAndGetBucketWithObjectLock() { - bucketStore.createBucket(TEST_BUCKET_NAME, true) + bucketStore.createBucket(TEST_BUCKET_NAME, true, BUCKET_OWNER_ENFORCED) val bucket = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) assertThat(bucket).isNotNull() @@ -99,7 +102,7 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testStoreAndGetBucketLifecycleConfiguration() { - bucketStore.createBucket(TEST_BUCKET_NAME, true) + bucketStore.createBucket(TEST_BUCKET_NAME, true, BUCKET_OWNER_ENFORCED) val filter1 = LifecycleRuleFilter(null, null, "documents/", null, null) val transition1 = Transition(null, 30, StorageClass.GLACIER) @@ -118,7 +121,7 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateAndDeleteBucket() { - bucketStore.createBucket(TEST_BUCKET_NAME, false) + bucketStore.createBucket(TEST_BUCKET_NAME, false, BUCKET_OWNER_ENFORCED) val bucketDeleted = bucketStore.deleteBucket(TEST_BUCKET_NAME) val bucket = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) 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 90ecee59d..194478174 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 @@ -15,7 +15,9 @@ */ package com.adobe.testing.s3mock.store +import com.adobe.testing.s3mock.dto.ChecksumAlgorithm import com.adobe.testing.s3mock.dto.CompletedPart +import com.adobe.testing.s3mock.dto.MultipartUpload import com.adobe.testing.s3mock.dto.Owner import com.adobe.testing.s3mock.dto.Part import com.adobe.testing.s3mock.dto.StorageClass @@ -165,6 +167,7 @@ internal class MultipartStoreTest : StoreTestBase() { emptyMap(), StorageClass.STANDARD, null, null ) val uploadId = multipartUpload.uploadId + val multipartUploadInfo = multipartUploadInfo(multipartUpload) multipartStore .putPart( metadataFrom(TEST_BUCKET_NAME), id, uploadId, "1", @@ -176,10 +179,10 @@ internal class MultipartStoreTest : StoreTestBase() { tempFile2, emptyMap() ) - val etag = + val result = multipartStore.completeMultipartUpload( metadataFrom(TEST_BUCKET_NAME), fileName, id, - uploadId, getParts(2), emptyMap() + uploadId, getParts(2), emptyMap(), multipartUploadInfo, "location" ) val allMd5s = DigestUtils.md5("Part1") + DigestUtils.md5("Part2") @@ -195,7 +198,7 @@ internal class MultipartStoreTest : StoreTestBase() { TEST_BUCKET_NAME, id.toString(), "objectMetadata.json" ).toFile() ).exists() - assertThat(etag).isEqualTo(DigestUtils.md5Hex(allMd5s) + "-2") + assertThat(result.etag).isEqualTo("\"${DigestUtils.md5Hex(allMd5s)}-2\"") } @Test @@ -216,6 +219,7 @@ internal class MultipartStoreTest : StoreTestBase() { emptyMap(), StorageClass.STANDARD, null, null ) val uploadId = multipartUpload.uploadId + val multipartUploadInfo = multipartUploadInfo(multipartUpload) multipartStore .putPart(metadataFrom(TEST_BUCKET_NAME), id, uploadId, "1", tempFile1, emptyMap()) multipartStore @@ -223,7 +227,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartStore.completeMultipartUpload( metadataFrom(TEST_BUCKET_NAME), fileName, id, uploadId, - getParts(2), emptyMap() + getParts(2), emptyMap(), multipartUploadInfo, "location" ) objectStore.getS3ObjectMetadata(metadataFrom(TEST_BUCKET_NAME), id).also { @@ -302,6 +306,7 @@ internal class MultipartStoreTest : StoreTestBase() { emptyMap(), StorageClass.STANDARD, null, null ) val uploadId = multipartUpload.uploadId + val multipartUploadInfo = multipartUploadInfo(multipartUpload) val tempFile = Files.createTempFile("", "") ByteArrayInputStream("Part1".toByteArray()).transferTo(Files.newOutputStream(tempFile)) multipartStore @@ -312,7 +317,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartStore.completeMultipartUpload( metadataFrom(TEST_BUCKET_NAME), fileName, id, uploadId, - getParts(1), emptyMap() + getParts(1), emptyMap(), multipartUploadInfo, "location" ) assertThat( @@ -335,7 +340,7 @@ internal class MultipartStoreTest : StoreTestBase() { StorageClass.STANDARD, null, null ) val uploadId = multipartUpload.uploadId - + val multipartUploadInfo = multipartUploadInfo(multipartUpload) val uploads = multipartStore.listMultipartUploads(bucketMetadata, NO_PREFIX) assertThat(uploads).hasSize(1) uploads.iterator().next().also { @@ -347,7 +352,7 @@ internal class MultipartStoreTest : StoreTestBase() { multipartStore.completeMultipartUpload( bucketMetadata, fileName, id, uploadId, getParts(0), - emptyMap() + emptyMap(), multipartUploadInfo, "location" ) assertThat(multipartStore.listMultipartUploads(bucketMetadata, NO_PREFIX)).isEmpty() @@ -371,6 +376,7 @@ internal class MultipartStoreTest : StoreTestBase() { StorageClass.STANDARD, null, null ) val uploadId1 = multipartUpload1.uploadId + val multipartUploadInfo1 = multipartUploadInfo(multipartUpload1) val fileName2 = "PartFile2" val id2 = managedId() val multipartUpload2 = multipartStore @@ -380,7 +386,7 @@ internal class MultipartStoreTest : StoreTestBase() { StorageClass.STANDARD, null, null ) val uploadId2 = multipartUpload2.uploadId - + val multipartUploadInfo2 = multipartUploadInfo(multipartUpload2) multipartStore.listMultipartUploads(bucketMetadata1, NO_PREFIX).also { assertThat(it).hasSize(1) it[0].also { @@ -403,11 +409,11 @@ internal class MultipartStoreTest : StoreTestBase() { multipartStore.completeMultipartUpload( bucketMetadata1, fileName1, id1, uploadId1, - getParts(0), emptyMap() + getParts(0), emptyMap(), multipartUploadInfo1, "location" ) multipartStore.completeMultipartUpload( bucketMetadata2, fileName2, id2, uploadId2, - getParts(0), emptyMap() + getParts(0), emptyMap(), multipartUploadInfo2, "location" ) assertThat(multipartStore.listMultipartUploads(bucketMetadata1, NO_PREFIX)).isEmpty() @@ -575,7 +581,7 @@ internal class MultipartStoreTest : StoreTestBase() { StorageClass.STANDARD, null, null ) val uploadId = multipartUpload.uploadId - + val multipartUploadInfo = multipartUploadInfo(multipartUpload) for (i in 1..10) { val tempFile = Files.createTempFile("", "") ByteArrayInputStream(("$i\n").toByteArray(StandardCharsets.UTF_8)) @@ -588,7 +594,7 @@ internal class MultipartStoreTest : StoreTestBase() { } multipartStore.completeMultipartUpload( metadataFrom(TEST_BUCKET_NAME), filename, id, uploadId, - getParts(10), emptyMap() + getParts(10), emptyMap(), multipartUploadInfo, "location" ) val s = objectStore.getS3ObjectMetadata(metadataFrom(TEST_BUCKET_NAME), id) .dataPath @@ -626,6 +632,18 @@ internal class MultipartStoreTest : StoreTestBase() { } + private fun multipartUploadInfo(multipartUpload: MultipartUpload?) = MultipartUploadInfo( + multipartUpload, + "application/octet-stream", + mapOf(), + mapOf(), + mapOf(), + "bucket", + null, + "checksum", + ChecksumAlgorithm.CRC32 + ) + companion object { private val idCache: MutableList = Collections.synchronizedList(arrayListOf()) 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 bb14855b9..4eb11db29 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 @@ -250,13 +250,13 @@ internal class ObjectStoreTest : StoreTestBase() { objectStore.copyS3Object( metadataFrom(sourceBucketName), sourceId, metadataFrom(destinationBucketName), - destinationId, destinationObjectName, emptyMap(), NO_USER_METADATA + destinationId, destinationObjectName, emptyMap(), NO_USER_METADATA, StorageClass.STANDARD_IA ) objectStore.getS3ObjectMetadata(metadataFrom(destinationBucketName), destinationId).also { assertThat(it.encryptionHeaders).isEmpty() assertThat(sourceFile).hasSameBinaryContentAs(it.dataPath.toFile()) - assertThat(it.storageClass).isEqualTo(StorageClass.GLACIER) + assertThat(it.storageClass).isEqualTo(StorageClass.STANDARD_IA) } } @@ -288,7 +288,8 @@ internal class ObjectStoreTest : StoreTestBase() { destinationId, destinationObjectName, encryptionHeaders(), - NO_USER_METADATA + NO_USER_METADATA, + StorageClass.STANDARD_IA ) objectStore.getS3ObjectMetadata(metadataFrom(destinationBucketName), destinationId).also { assertThat(it.encryptionHeaders).isEqualTo(encryptionHeaders()) diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt index 1a19db376..bd938a9f5 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoreConfigurationTest.kt @@ -20,6 +20,7 @@ import org.apache.commons.io.FileUtils import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir +import software.amazon.awssdk.services.s3.model.ObjectOwnership import java.io.IOException import java.nio.file.Files import java.nio.file.Path @@ -60,7 +61,7 @@ internal class StoreConfigurationTest { val bucketMetadata = BucketMetadata( existingBucketName, Instant.now().toString(), - null, null, existingBucket + null, null, ObjectOwnership.BUCKET_OWNER_ENFORCED, existingBucket ) val metaFile = Paths.get(existingBucket.toString(), BUCKET_META_FILE) OBJECT_MAPPER.writeValue(metaFile.toFile(), bucketMetadata) 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 c3ddb424a..1e32619a4 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 @@ -21,6 +21,7 @@ import org.apache.http.entity.ContentType import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpHeaders import org.springframework.http.MediaType +import software.amazon.awssdk.services.s3.model.ObjectOwnership import java.io.File import java.nio.file.Paths import java.util.Date @@ -36,6 +37,7 @@ internal abstract class StoreTestBase { Date().toString(), null, null, + ObjectOwnership.BUCKET_OWNER_ENFORCED, Paths.get(rootFolder.toString(), bucketName), mapOf() ) diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoresWithExistingFileRootTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoresWithExistingFileRootTest.kt index 15a578857..8651aea89 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoresWithExistingFileRootTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/store/StoresWithExistingFileRootTest.kt @@ -27,6 +27,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.context.annotation.Bean +import software.amazon.awssdk.services.s3.model.ObjectOwnership import java.io.File import java.util.UUID @@ -49,7 +50,7 @@ internal class StoresWithExistingFileRootTest : StoreTestBase() { @Test fun testBucketStoreWithExistingRoot() { - bucketStore.createBucket(TEST_BUCKET_NAME, false) + bucketStore.createBucket(TEST_BUCKET_NAME, false, ObjectOwnership.BUCKET_OWNER_ENFORCED) val bucket = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) assertThatThrownBy { testBucketStore.getBucketMetadata(TEST_BUCKET_NAME) } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/util/DigestUtilTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/util/DigestUtilTest.kt index 48f152684..278b2b977 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/util/DigestUtilTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/util/DigestUtilTest.kt @@ -19,6 +19,8 @@ import org.apache.commons.codec.digest.DigestUtils import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInfo +import software.amazon.awssdk.core.checksums.Algorithm +import software.amazon.awssdk.utils.BinaryUtils internal class DigestUtilTest { @Test @@ -41,4 +43,25 @@ internal class DigestUtilTest { assertThat(DigestUtil.hexDigestMultipart(files)).isEqualTo(expected) } + + @Test + fun testChecksumOfMultipleFiles(testInfo: TestInfo) { + //yes, this is correct - AWS calculates a Multipart digest by calculating the digest of every + //file involved, and then calculates the digest on the result. + //a hyphen with the part count is added as a suffix. + val expected = "${ + BinaryUtils.toBase64(DigestUtils.sha256( + DigestUtils.sha256("Part1") //testFile1 + + DigestUtils.sha256("Part2") //testFile2 + )) + }-2" + + //files contain the exact content seen above + val files = listOf( + TestUtil.getTestFile(testInfo, "testFile1").toPath(), + TestUtil.getTestFile(testInfo, "testFile2").toPath() + ) + + assertThat(DigestUtil.checksumMultipart(files, Algorithm.SHA256)).isEqualTo(expected) + } } diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_multiPart.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_multiPart.xml index e53e2d785..32097d16d 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_multiPart.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_multiPart.xml @@ -17,7 +17,7 @@ --> - "etag" + etag 1000 false diff --git a/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_object.xml b/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_object.xml index b2ec1ff6a..9dda4df78 100644 --- a/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_object.xml +++ b/server/src/test/resources/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest_testSerialization_object.xml @@ -17,7 +17,7 @@ --> - "etag" + etag STANDARD 1 diff --git a/server/src/test/resources/com/adobe/testing/s3mock/util/DigestUtilTest_testChecksumOfMultipleFiles_testFile1 b/server/src/test/resources/com/adobe/testing/s3mock/util/DigestUtilTest_testChecksumOfMultipleFiles_testFile1 new file mode 100644 index 000000000..4b668a1ed --- /dev/null +++ b/server/src/test/resources/com/adobe/testing/s3mock/util/DigestUtilTest_testChecksumOfMultipleFiles_testFile1 @@ -0,0 +1 @@ +Part1 \ No newline at end of file diff --git a/server/src/test/resources/com/adobe/testing/s3mock/util/DigestUtilTest_testChecksumOfMultipleFiles_testFile2 b/server/src/test/resources/com/adobe/testing/s3mock/util/DigestUtilTest_testChecksumOfMultipleFiles_testFile2 new file mode 100644 index 000000000..86dbe2084 --- /dev/null +++ b/server/src/test/resources/com/adobe/testing/s3mock/util/DigestUtilTest_testChecksumOfMultipleFiles_testFile2 @@ -0,0 +1 @@ +Part2 \ No newline at end of file