diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..e00921d00 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,22 @@ +name: stale.yml +on: + schedule: + - cron: '0 0 * * *' # Run every day at midnight + +permissions: + issues: write + pull-requests: write + +jobs: + close_stale_prs: + runs-on: ubuntu-latest + steps: + - name: Close stale issues and pull requests + uses: actions/stale@v9.1.0 + with: + days-before-stale: 30 + days-before-close: 5 + stale-issue-message: 'This issue has been flagged as stale due to over 30 days of inactivity. If no updates or comments are made within the next 5 days, the system will automatically close it to maintain repository hygiene.' + close-issue-message: 'This issue has been automatically closed due to prolonged inactivity. It was previously marked as stale after 30 days without activity and has now been closed following an additional 5-day grace period. If you believe this issue should be reopened, please provide a comment with relevant updates or justification.' + stale-pr-message: 'This pull request has been marked as stale due to 30 days of inactivity. If no further updates or comments are made within the next 5 days, it will be automatically closed to maintain repository hygiene and reduce review backlog.' + close-pr-message: 'This pull request has been automatically closed due to extended inactivity. It was previously flagged as stale after 30 days without activity and has now been closed following a 5-day grace period. If you believe this pull request is still relevant, feel free to reopen it or submit a new one with updated context.' diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt index b71358234..2347932b6 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AclIT.kt @@ -57,12 +57,12 @@ internal class AclIT : S3TestBase() { s3Client.getObjectAcl { it.bucket(bucketName) it.key(sourceKey) - }.also { - assertThat(it.sdkHttpResponse().isSuccessful).isTrue() - assertThat(it.owner().id()).isNotBlank() - assertThat(it.owner().displayName()).isNotBlank() - assertThat(it.grants().size).isEqualTo(1) - assertThat(it.grants()[0].permission()).isEqualTo(FULL_CONTROL) + }.also { resp -> + assertThat(resp.sdkHttpResponse().isSuccessful).isTrue() + assertThat(resp.owner().id()).isNotBlank() + assertThat(resp.owner().displayName()).isNotBlank() + assertThat(resp.grants()).hasSize(1) + assertThat(resp.grants().first().permission()).isEqualTo(FULL_CONTROL) } } @@ -87,13 +87,13 @@ internal class AclIT : S3TestBase() { assertThat(it).hasSize(1) } - acl.grants()[0].also { - assertThat(it.permission()).isEqualTo(FULL_CONTROL) - }.grantee().also { - assertThat(it).isNotNull - assertThat(it.id()).isEqualTo(DEFAULT_OWNER.id) - assertThat(it.displayName()).isEqualTo(DEFAULT_OWNER.displayName) - assertThat(it.type()).isEqualTo(CANONICAL_USER) + acl.grants().first().also { grant -> + assertThat(grant.permission()).isEqualTo(FULL_CONTROL) + }.grantee().also { grantee -> + assertThat(grantee).isNotNull + assertThat(grantee.id()).isEqualTo(DEFAULT_OWNER.id) + assertThat(grantee.displayName()).isEqualTo(DEFAULT_OWNER.displayName) + assertThat(grantee.type()).isEqualTo(CANONICAL_USER) } } @@ -108,7 +108,6 @@ internal class AclIT : S3TestBase() { val userName = "John Doe" val granteeId = "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2ef" val granteeName = "Jane Doe" - "jane@doe.com" s3Client.putObjectAcl { it.bucket(bucketName) it.key(sourceKey) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AwsChunkedEncodingIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AwsChunkedEncodingIT.kt index b636f7159..ebc99f306 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AwsChunkedEncodingIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/AwsChunkedEncodingIT.kt @@ -58,9 +58,9 @@ internal class AwsChunkedEncodingIT : S3TestBase() { RequestBody.fromFile(UPLOAD_FILE) ) - putObjectResponse.checksumSHA256().also { - assertThat(it).isNotBlank - assertThat(it).isEqualTo(expectedChecksum) + putObjectResponse.checksumSHA256().also { checksum -> + assertThat(checksum).isNotBlank() + assertThat(checksum).isEqualTo(expectedChecksum) } s3Client.getObject { @@ -71,9 +71,9 @@ internal class AwsChunkedEncodingIT : S3TestBase() { assertThat(it.response().eTag()).isEqualTo(expectedEtag) assertThat(it.response().contentLength()).isEqualTo(UPLOAD_FILE_LENGTH) assertThat(it.response().contentEncoding()).isNotEqualTo("aws-chunked") - it.response().checksumSHA256().also { - assertThat(it).isNotBlank - assertThat(it).isEqualTo(expectedChecksum) + it.response().checksumSHA256().also { checksum -> + assertThat(checksum).isNotBlank() + assertThat(checksum).isEqualTo(expectedChecksum) } } } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt index d49487001..510752854 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/BucketIT.kt @@ -56,19 +56,25 @@ internal class BucketIT : S3TestBase() { val bucketName = bucketName(testInfo) s3Client.createBucket { it.bucket(bucketName) } - val bucketCreated = s3Client.waiter().waitUntilBucketExists { it.bucket(bucketName) } - val bucketCreatedResponse = bucketCreated.matched().response().get() + val bucketCreatedResponse = s3Client + .waiter() + .waitUntilBucketExists { it.bucket(bucketName) } + .matched() + .response() + .get() assertThat(bucketCreatedResponse).isNotNull //does not throw exception if bucket exists. s3Client.headBucket { it.bucket(bucketName) } s3Client.deleteBucket { it.bucket(bucketName) } - val bucketDeleted = s3Client.waiter().waitUntilBucketNotExists { it.bucket(bucketName) } - bucketDeleted.matched().exception().get().also { - assertThat(it).isNotNull - assertThat(it).isInstanceOf(NoSuchBucketException::class.java) - } + val deletionException = s3Client + .waiter() + .waitUntilBucketNotExists { it.bucket(bucketName) } + .matched() + .exception() + .get() + assertThat(deletionException).isInstanceOf(NoSuchBucketException::class.java) } /** @@ -82,23 +88,27 @@ internal class BucketIT : S3TestBase() { val bucketName = bucketName(testInfo) val createBucketResponse = s3Client.createBucket { it.bucket(bucketName) - it.createBucketConfiguration { - it.locationConstraint("ap-southeast-5") - it.bucket { - it.dataRedundancy(DataRedundancy.SINGLE_AVAILABILITY_ZONE) - it.type(BucketType.DIRECTORY) + it.createBucketConfiguration { cfg -> + cfg.locationConstraint("ap-southeast-5") + cfg.bucket { b -> + b.dataRedundancy(DataRedundancy.SINGLE_AVAILABILITY_ZONE) + b.type(BucketType.DIRECTORY) } - it.location { - it.name("SomeName") - it.type(LocationType.AVAILABILITY_ZONE) + cfg.location { loc -> + loc.name("SomeName") + loc.type(LocationType.AVAILABILITY_ZONE) } } } assertThat(createBucketResponse.sdkHttpResponse().statusCode()).isEqualTo(200) assertThat(createBucketResponse.location()).isEqualTo("/$bucketName") - val bucketCreated = s3Client.waiter().waitUntilBucketExists { it.bucket(bucketName) } - val bucketCreatedResponse = bucketCreated.matched().response().get() + val bucketCreatedResponse = s3Client + .waiter() + .waitUntilBucketExists { it.bucket(bucketName) } + .matched() + .response() + .get() assertThat(bucketCreatedResponse).isNotNull //does not throw exception if bucket exists. @@ -353,10 +363,13 @@ internal class BucketIT : S3TestBase() { val bucketName = bucketName(testInfo) s3Client.createBucket { it.bucket(bucketName) } - val bucketCreated = s3Client.waiter().waitUntilBucketExists { it.bucket(bucketName) } - bucketCreated.matched().response().get().also { - assertThat(it).isNotNull - } + val createdResponse = s3Client + .waiter() + .waitUntilBucketExists { it.bucket(bucketName) } + .matched() + .response() + .get() + assertThat(createdResponse).isNotNull assertThatThrownBy { s3Client.createBucket { it.bucket(bucketName) } @@ -389,11 +402,13 @@ internal class BucketIT : S3TestBase() { } s3Client.deleteBucket { it.bucket(bucketName) } - val bucketDeleted = s3Client.waiter().waitUntilBucketNotExists { it.bucket(bucketName) } - bucketDeleted.matched().exception().get().also { - assertThat(it).isNotNull - assertThat(it).isInstanceOf(NoSuchBucketException::class.java) - } + val deletionException = s3Client + .waiter() + .waitUntilBucketNotExists { it.bucket(bucketName) } + .matched() + .exception() + .get() + assertThat(deletionException).isInstanceOf(NoSuchBucketException::class.java) assertThatThrownBy { s3Client.deleteBucket(DeleteBucketRequest.builder().bucket(bucketName).build()) @@ -412,8 +427,11 @@ internal class BucketIT : S3TestBase() { val bucketName = bucketName(testInfo) s3Client.createBucket { it.bucket(bucketName) } - val bucketCreated = s3Client.waiter().waitUntilBucketExists { it.bucket(bucketName) } - val bucketCreatedResponse = bucketCreated.matched().response()!!.get() + val bucketCreatedResponse = s3Client + .waiter() + .waitUntilBucketExists { it.bucket(bucketName) } + .matched() + .response()!!.get() assertThat(bucketCreatedResponse).isNotNull assertThatThrownBy { @@ -433,10 +451,12 @@ internal class BucketIT : S3TestBase() { val bucketName = bucketName(testInfo) s3Client.createBucket { it.bucket(bucketName) } - val bucketCreated = s3Client.waiter().waitUntilBucketExists { it.bucket(bucketName) } - bucketCreated.matched().response()!!.get().also { - assertThat(it).isNotNull - } + val createdResponse = s3Client + .waiter() + .waitUntilBucketExists { it.bucket(bucketName) } + .matched() + .response()!!.get() + assertThat(createdResponse).isNotNull val configuration = BucketLifecycleConfiguration .builder() diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ConcurrencyIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ConcurrencyIT.kt index fc44764a7..990d35f45 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ConcurrencyIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ConcurrencyIT.kt @@ -40,21 +40,18 @@ internal class ConcurrencyIT : S3TestBase() { ) fun `concurrent bucket puts, gets and deletes are successful`(testInfo: TestInfo) { val bucketName = givenBucket(testInfo) - val runners = mutableListOf() + val runners = (1..100).map { Runner(bucketName, "test/key$it") } val pool = Executors.newFixedThreadPool(100) - for (i in 1..100) { - runners.add(Runner(bucketName, "test/key$i")) - } val futures = pool.invokeAll(runners) - assertThat(futures).hasSize(100).allSatisfy { - assertThat(it.get()).isTrue + assertThat(futures).hasSize(100).allSatisfy { future -> + assertThat(future.get()).isTrue } assertThat(DONE.get()).isEqualTo(100) } companion object { - val LATCH = CountDownLatch(100) - val DONE = AtomicInteger(0) + private val LATCH = CountDownLatch(100) + private val DONE = AtomicInteger(0) } inner class Runner(val bucketName: String, val key: String) : Callable { @@ -65,8 +62,8 @@ internal class ConcurrencyIT : S3TestBase() { it.bucket(bucketName) it.key(key) }, RequestBody.empty() - ).also { - assertThat(it.eTag()).isNotBlank + ).let { response -> + assertThat(response.eTag()).isNotBlank } s3Client.getObject { @@ -79,8 +76,8 @@ internal class ConcurrencyIT : S3TestBase() { s3Client.deleteObject { it.bucket(bucketName) it.key(key) - }.also { - assertThat(it.deleteMarker()).isTrue + }.let { response -> + assertThat(response.deleteMarker()).isTrue } DONE.incrementAndGet() return true diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectIT.kt index c13c31df1..cb849c4d0 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CopyObjectIT.kt @@ -30,7 +30,6 @@ import software.amazon.awssdk.services.s3.model.MetadataDirective import software.amazon.awssdk.services.s3.model.S3Exception import software.amazon.awssdk.services.s3.model.ServerSideEncryption import software.amazon.awssdk.services.s3.model.StorageClass -import java.io.File import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit.SECONDS @@ -58,8 +57,8 @@ internal class CopyObjectIT : S3TestBase() { it.sourceKey(sourceKey) it.destinationBucket(destinationBucketName) it.destinationKey(destinationKey) - }.copyObjectResult().eTag().also { - assertThat(it).isEqualTo(putObjectResult.eTag()) + }.copyObjectResult().eTag().let { eTag -> + assertThat(eTag).isEqualTo(putObjectResult.eTag()) } s3Client.getObject { @@ -91,8 +90,8 @@ internal class CopyObjectIT : S3TestBase() { it.sourceKey(sourceKey) it.destinationBucket(destinationBucketName) it.destinationKey(destinationKey) - }.copyObjectResult().eTag().also { - assertThat(it).isEqualTo(putObjectResult.eTag()) + }.copyObjectResult().eTag().let { eTag -> + assertThat(eTag).isEqualTo(putObjectResult.eTag()) } s3Client.getObject { @@ -120,8 +119,8 @@ internal class CopyObjectIT : S3TestBase() { it.destinationBucket(destinationBucketName) it.destinationKey(destinationKey) it.copySourceIfMatch(matchingEtag) - }.copyObjectResult().eTag().also { - assertThat(it).isEqualTo(putObjectResult.eTag()) + }.copyObjectResult().eTag().let { eTag -> + assertThat(eTag).isEqualTo(putObjectResult.eTag()) } s3Client.getObject { @@ -151,8 +150,8 @@ internal class CopyObjectIT : S3TestBase() { it.destinationKey(destinationKey) it.copySourceIfMatch(matchingEtag) it.copySourceIfUnmodifiedSince(now) - }.copyObjectResult().eTag().also { - assertThat(it).isEqualTo(putObjectResult.eTag()) + }.copyObjectResult().eTag().let { eTag -> + assertThat(eTag).isEqualTo(putObjectResult.eTag()) } s3Client.getObject { @@ -181,8 +180,8 @@ internal class CopyObjectIT : S3TestBase() { it.destinationBucket(destinationBucketName) it.destinationKey(destinationKey) it.copySourceIfModifiedSince(now) - }.copyObjectResult().eTag().also { - assertThat(it).isEqualTo(putObjectResult.eTag()) + }.copyObjectResult().eTag().let { eTag -> + assertThat(eTag).isEqualTo(putObjectResult.eTag()) } s3Client.getObject { @@ -238,8 +237,8 @@ internal class CopyObjectIT : S3TestBase() { it.destinationBucket(destinationBucketName) it.destinationKey(destinationKey) it.copySourceIfUnmodifiedSince(now) - }.copyObjectResult().eTag().also { - assertThat(it).isEqualTo(putObjectResult.eTag()) + }.copyObjectResult().eTag().let { eTag -> + assertThat(eTag).isEqualTo(putObjectResult.eTag()) } s3Client.getObject { @@ -266,8 +265,8 @@ internal class CopyObjectIT : S3TestBase() { it.destinationBucket(destinationBucketName) it.destinationKey(destinationKey) it.copySourceIfNoneMatch(noneMatchingEtag) - }.copyObjectResult().eTag().also { - assertThat(it).isEqualTo(putObjectResult.eTag()) + }.copyObjectResult().eTag().let { eTag -> + assertThat(eTag).isEqualTo(putObjectResult.eTag()) } s3Client.getObject { diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsIT.kt index a409b8b73..6a121d921 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CorsIT.kt @@ -22,7 +22,6 @@ import org.apache.http.client.methods.HttpOptions import org.apache.http.client.methods.HttpPut import org.apache.http.entity.ByteArrayEntity import org.apache.http.impl.client.CloseableHttpClient -import org.apache.http.message.BasicHeader import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInfo @@ -40,8 +39,8 @@ internal class CorsIT : S3TestBase() { reason = "No credentials sent in plain HTTP request") fun testPutObject_cors(testInfo: TestInfo) { val bucketName = givenBucket(testInfo) - val optionsRequest = HttpOptions("$serviceEndpoint/${bucketName}/testObjectName").apply { - this.addHeader("Origin", "http://localhost/") + val optionsRequest = HttpOptions("$serviceEndpoint/$bucketName/testObjectName").apply { + addHeader("Origin", "http://localhost/") } httpClient.execute(optionsRequest).also { assertThat(it.getFirstHeader("Allow").value).contains("PUT") @@ -50,8 +49,8 @@ internal class CorsIT : S3TestBase() { val byteArray = UUID.randomUUID().toString().toByteArray() val expectedEtag = "\"${DigestUtil.hexDigest(byteArray)}\"" val putObject = HttpPut("$serviceEndpoint/$bucketName/testObjectName").apply { - this.entity = ByteArrayEntity(byteArray) - this.addHeader("Origin", "http://localhost/") + entity = ByteArrayEntity(byteArray) + addHeader("Origin", "http://localhost/") } httpClient.execute(putObject).use { @@ -68,9 +67,9 @@ internal class CorsIT : S3TestBase() { fun testGetBucket_cors(testInfo: TestInfo) { val targetBucket = givenBucket(testInfo) val httpOptions = HttpOptions("$serviceEndpoint/$targetBucket").apply { - this.addHeader(BasicHeader("Origin", "http://someurl.com")) - this.addHeader(BasicHeader("Access-Control-Request-Method", "GET")) - this.addHeader(BasicHeader("Access-Control-Request-Headers", "Content-Type, x-requested-with")) + addHeader("Origin", "http://someurl.com") + addHeader("Access-Control-Request-Method", "GET") + addHeader("Access-Control-Request-Headers", "Content-Type, x-requested-with") } httpClient.execute(httpOptions).use { diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncIT.kt index eba884c3c..328d1fd2e 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/CrtAsyncIT.kt @@ -37,7 +37,7 @@ internal class CrtAsyncIT : S3TestBase() { @Test @S3VerifiedSuccess(year = 2025) - fun testPutObject_etagCreation(testInfo: TestInfo) { + fun testPutObject_etagCreation() { val expectedEtag = UPLOAD_FILE.inputStream().use { "\"${DigestUtil.hexDigest(it)}\"" } @@ -64,7 +64,7 @@ internal class CrtAsyncIT : S3TestBase() { @Test @S3VerifiedSuccess(year = 2025) - fun testPutGetObject_successWithMatchingEtag(testInfo: TestInfo) { + fun testPutGetObject_successWithMatchingEtag() { val bucketName = randomName autoS3CrtAsyncClient .createBucket { @@ -95,7 +95,7 @@ internal class CrtAsyncIT : S3TestBase() { @S3VerifiedSuccess(year = 2025) fun testMultipartUpload(testInfo: TestInfo) { val bucketName = givenBucket(testInfo) - val objectMetadata = mapOf(Pair("key", "value")) + val objectMetadata = mapOf("key" to "value") val createMultipartUploadResponseCompletableFuture = autoS3CrtAsyncClient .createMultipartUpload { it.bucket(bucketName) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt index 72919b222..047bc4c67 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt @@ -102,7 +102,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() { fun testPutGetHeadDeleteObjects(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val bucketName = givenBucket(testInfo) - val keys = listOf("${key}-1", "${key}-2", "${key}-3") + val keys = listOf("$key-1", "$key-2", "$key-3") keys.forEach { key -> s3Client.putObject({ it.bucket(bucketName) @@ -116,9 +116,9 @@ internal class GetPutDeleteObjectIT : S3TestBase() { it.bucket(bucketName) it.delete { it.objects( - { it.key("${key}-1") }, - { it.key("${key}-2") }, - { it.key("${key}-3") }, + { it.key("$key-1") }, + { it.key("$key-2") }, + { it.key("$key-3") }, ) } } @@ -652,11 +652,11 @@ internal class GetPutDeleteObjectIT : S3TestBase() { private fun PutObjectRequest.Builder .checksum(checksum: String, checksumAlgorithm: ChecksumAlgorithm): PutObjectRequest.Builder = when (checksumAlgorithm) { - ChecksumAlgorithm.SHA1 -> this.checksumSHA1(checksum) - ChecksumAlgorithm.SHA256 -> this.checksumSHA256(checksum) - ChecksumAlgorithm.CRC32 -> this.checksumCRC32(checksum) - ChecksumAlgorithm.CRC32_C -> this.checksumCRC32C(checksum) - ChecksumAlgorithm.CRC64_NVME -> this.checksumCRC64NVME(checksum) + ChecksumAlgorithm.SHA1 -> checksumSHA1(checksum) + ChecksumAlgorithm.SHA256 -> checksumSHA256(checksum) + ChecksumAlgorithm.CRC32 -> checksumCRC32(checksum) + ChecksumAlgorithm.CRC32_C -> checksumCRC32C(checksum) + ChecksumAlgorithm.CRC64_NVME -> checksumCRC64NVME(checksum) else -> error("Unknown checksum algorithm") } @@ -811,7 +811,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() { @Test @S3VerifiedSuccess(year = 2025) - fun testPutGetDeleteObject_twoBuckets(testInfo: TestInfo) { + fun testPutGetDeleteObject_twoBuckets() { val bucket1 = givenBucket() val bucket2 = givenBucket() givenObject(bucket1, UPLOAD_FILE_NAME) @@ -831,7 +831,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() { @Test @S3VerifiedSuccess(year = 2025) - fun testPutGetHeadObject_storeHeaders(testInfo: TestInfo) { + fun testPutGetHeadObject_storeHeaders() { val bucket = givenBucket() val contentDisposition = ContentDisposition.formData() .name("file") @@ -1294,7 +1294,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() { val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME) val matchingEtag = putObjectResponse.eTag() - val noneMatchingEtag = "\"${randomName}\"" + val noneMatchingEtag = "\"$randomName\"" s3Client.getObject { it.bucket(bucketName) @@ -1417,7 +1417,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() { }.use { assertThat(it.response().contentLength()).isEqualTo(smallRequestEndBytes) assertThat(it.response().contentRange()) - .isEqualTo("bytes $smallRequestStartBytes-$smallRequestEndBytes/${UPLOAD_FILE_LENGTH}") + .isEqualTo("bytes $smallRequestStartBytes-$smallRequestEndBytes/$UPLOAD_FILE_LENGTH") } val largeRequestStartBytes = 0L @@ -1431,7 +1431,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() { assertThat(it.response().contentLength()).isEqualTo(min(UPLOAD_FILE_LENGTH, largeRequestEndBytes + 1)) assertThat(it.response().contentRange()) .isEqualTo( - "bytes $largeRequestStartBytes-${min(UPLOAD_FILE_LENGTH - 1, largeRequestEndBytes)}/${UPLOAD_FILE_LENGTH}" + "bytes $largeRequestStartBytes-${min(UPLOAD_FILE_LENGTH - 1, largeRequestEndBytes)}/$UPLOAD_FILE_LENGTH" ) } } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/KotlinSDKIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/KotlinSDKIT.kt index 6ac80e490..f567527e5 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/KotlinSDKIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/KotlinSDKIT.kt @@ -27,25 +27,25 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInfo -internal class KotlinSDKIT: S3TestBase() { +internal class KotlinSDKIT : S3TestBase() { private val s3Client = createS3ClientKotlin() @Test @S3VerifiedFailure(year = 2025, reason = "The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.") - fun createAndDeleteBucket(testInfo: TestInfo) : Unit = runBlocking { + fun createAndDeleteBucket(testInfo: TestInfo): Unit = runBlocking { val bucketName = bucketName(testInfo) s3Client.createBucket(CreateBucketRequest { bucket = bucketName }) s3Client.waitUntilBucketExists(HeadBucketRequest { bucket = bucketName }) - //does not throw exception if bucket exists. + // does not throw exception if bucket exists. s3Client.headBucket(HeadBucketRequest { bucket = bucketName }) s3Client.deleteBucket(DeleteBucketRequest { bucket = bucketName }) s3Client.waitUntilBucketNotExists(HeadBucketRequest { bucket = bucketName }) - //throws exception if bucket does not exist. + // throws exception if bucket does not exist. assertThatThrownBy { runBlocking { s3Client.headBucket(HeadBucketRequest { bucket = bucketName }) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldIT.kt index e14581695..fb7c2afe9 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/LegalHoldIT.kt @@ -16,7 +16,6 @@ package com.adobe.testing.s3mock.its -import com.adobe.testing.s3mock.util.DigestUtil import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test @@ -25,7 +24,6 @@ import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.ObjectLockLegalHoldStatus import software.amazon.awssdk.services.s3.model.S3Exception -import java.io.File internal class LegalHoldIT : S3TestBase() { diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt index 059ea52b0..2fd5d3120 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ListObjectsIT.kt @@ -31,8 +31,6 @@ import software.amazon.awssdk.services.s3.model.EncodingType import software.amazon.awssdk.services.s3.model.NoSuchBucketException import software.amazon.awssdk.services.s3.model.S3Object import software.amazon.awssdk.utils.http.SdkHttpUtils -import java.io.File -import java.util.stream.Collectors internal class ListObjectsIT : S3TestBase() { private val s3Client: S3Client = createS3Client() @@ -117,7 +115,7 @@ internal class ListObjectsIT : S3TestBase() { val bucketName = givenBucket(testInfo) val weirdStuff = charsSafe() val prefix = "shouldListWithCorrectObjectNames/" - val key = "$prefix$weirdStuff${UPLOAD_FILE_NAME}$weirdStuff" + val key = "$prefix$weirdStuff$UPLOAD_FILE_NAME$weirdStuff" s3Client.putObject( { it.bucket(bucketName) @@ -149,7 +147,7 @@ internal class ListObjectsIT : S3TestBase() { val bucketName = givenBucket(testInfo) val weirdStuff = charsSafe() val prefix = "shouldListWithCorrectObjectNames/" - val key = "$prefix$weirdStuff${UPLOAD_FILE_NAME}$weirdStuff" + val key = "$prefix$weirdStuff$UPLOAD_FILE_NAME$weirdStuff" s3Client.putObject( { it.bucket(bucketName) @@ -185,7 +183,7 @@ internal class ListObjectsIT : S3TestBase() { val bucketName = givenBucket(testInfo) val weirdStuff = "\u0001" // key invalid in XML val prefix = "shouldHonorEncodingTypeV1/" - val key = "$prefix$weirdStuff${UPLOAD_FILE_NAME}$weirdStuff" + val key = "$prefix$weirdStuff$UPLOAD_FILE_NAME$weirdStuff" s3Client.putObject( { it.bucket(bucketName) @@ -221,7 +219,7 @@ internal class ListObjectsIT : S3TestBase() { val bucketName = givenBucket(testInfo) val weirdStuff = "\u0001" // key invalid in XML val prefix = "shouldHonorEncodingTypeV2/" - val key = "$prefix$weirdStuff${UPLOAD_FILE_NAME}$weirdStuff" + val key = "$prefix$weirdStuff$UPLOAD_FILE_NAME$weirdStuff" s3Client.putObject( { it.bucket(bucketName) @@ -272,17 +270,15 @@ internal class ListObjectsIT : S3TestBase() { parameters.prefix, parameters.delimiter, parameters.startAfter, - listing.contents().stream().map { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) } - .collect(Collectors.joining("\n ")), - java.lang.String.join("\n ", listing.commonPrefixes().map(CommonPrefix::prefix)) + listing.contents().joinToString("\n ") { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) }, + listing.commonPrefixes().joinToString("\n ", transform = CommonPrefix::prefix) ) listing.commonPrefixes().also { - assertThat(it.stream().map { s: CommonPrefix -> SdkHttpUtils.urlDecode(s.prefix()) } - .collect(Collectors.toList())) + assertThat(it.map { s: CommonPrefix -> SdkHttpUtils.urlDecode(s.prefix()) }) .containsExactlyInAnyOrder(*parameters.expectedPrefixes) } listing.contents().also { - assertThat(it.stream().map { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) }.toList()).isEqualTo(listOf(*expectedDecodedKeys)) + assertThat(it.map { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) }).isEqualTo(listOf(*expectedDecodedKeys)) } if (parameters.expectedEncoding != null) { assertThat(listing.encodingType().toString()).isEqualTo(parameters.expectedEncoding) @@ -322,17 +318,15 @@ internal class ListObjectsIT : S3TestBase() { parameters.prefix, parameters.delimiter, parameters.startAfter, - listing.contents().stream().map { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) } - .collect(Collectors.joining("\n ")), - java.lang.String.join("\n ", listing.commonPrefixes().map(CommonPrefix::prefix)) + listing.contents().joinToString("\n ") { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) }, + listing.commonPrefixes().joinToString("\n ", transform = CommonPrefix::prefix) ) listing.commonPrefixes().also { - assertThat(it.stream().map { s: CommonPrefix -> SdkHttpUtils.urlDecode(s.prefix()) } - .collect(Collectors.toList())) + assertThat(it.map { s: CommonPrefix -> SdkHttpUtils.urlDecode(s.prefix()) }) .containsExactlyInAnyOrder(*parameters.expectedPrefixes) } listing.contents().also { - assertThat(it.stream().map { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) }.toList()).isEqualTo(listOf(*expectedDecodedKeys)) + assertThat(it.map { s: S3Object -> SdkHttpUtils.urlDecode(s.key()) }).isEqualTo(listOf(*expectedDecodedKeys)) } if (parameters.expectedEncoding != null) { assertThat(listing.encodingType().toString()).isEqualTo(parameters.expectedEncoding) diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt index 4193a0a2b..924859940 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultipartIT.kt @@ -140,7 +140,7 @@ internal class MultipartIT : S3TestBase() { @S3VerifiedSuccess(year = 2025) fun testMultipartUpload_withUserMetadata(testInfo: TestInfo) { val bucketName = givenBucket(testInfo) - val objectMetadata = mapOf(Pair("key", "value")) + val objectMetadata = mapOf("key" to "value") val initiateMultipartUploadResult = s3Client .createMultipartUpload { it.bucket(bucketName) @@ -594,11 +594,11 @@ internal class MultipartIT : S3TestBase() { checksumAlgorithm: ChecksumAlgorithm ): UploadPartRequest.Builder = when (checksumAlgorithm) { - ChecksumAlgorithm.SHA1 -> this.checksumSHA1(checksum) - ChecksumAlgorithm.SHA256 -> this.checksumSHA256(checksum) - ChecksumAlgorithm.CRC32 -> this.checksumCRC32(checksum) - ChecksumAlgorithm.CRC32_C -> this.checksumCRC32C(checksum) - ChecksumAlgorithm.CRC64_NVME -> this.checksumCRC64NVME(checksum) + ChecksumAlgorithm.SHA1 -> checksumSHA1(checksum) + ChecksumAlgorithm.SHA256 -> checksumSHA256(checksum) + ChecksumAlgorithm.CRC32 -> checksumCRC32(checksum) + ChecksumAlgorithm.CRC32_C -> checksumCRC32C(checksum) + ChecksumAlgorithm.CRC64_NVME -> checksumCRC64NVME(checksum) else -> error("Unknown checksum algorithm") } @@ -607,11 +607,11 @@ internal class MultipartIT : S3TestBase() { checksumAlgorithm: ChecksumAlgorithm ): CompleteMultipartUploadRequest.Builder = when (checksumAlgorithm) { - ChecksumAlgorithm.SHA1 -> this.checksumSHA1(checksum) - ChecksumAlgorithm.SHA256 -> this.checksumSHA256(checksum) - ChecksumAlgorithm.CRC32 -> this.checksumCRC32(checksum) - ChecksumAlgorithm.CRC32_C -> this.checksumCRC32C(checksum) - ChecksumAlgorithm.CRC64_NVME -> this.checksumCRC64NVME(checksum) + ChecksumAlgorithm.SHA1 -> checksumSHA1(checksum) + ChecksumAlgorithm.SHA256 -> checksumSHA256(checksum) + ChecksumAlgorithm.CRC32 -> checksumCRC32(checksum) + ChecksumAlgorithm.CRC32_C -> checksumCRC32C(checksum) + ChecksumAlgorithm.CRC64_NVME -> checksumCRC64NVME(checksum) else -> error("Unknown checksum algorithm") } @@ -619,7 +619,7 @@ internal class MultipartIT : S3TestBase() { @S3VerifiedSuccess(year = 2025) fun `list parts lists all uploaded parts`(testInfo: TestInfo) { val bucketName = givenBucket(testInfo) - val objectMetadata = mapOf(Pair("key", "value")) + val objectMetadata = mapOf("key" to "value") val hash = UPLOAD_FILE.inputStream().use { DigestUtils.md5Hex(it) } val initiateMultipartUploadResult = s3Client.createMultipartUpload { it.bucket(bucketName) @@ -662,13 +662,13 @@ internal class MultipartIT : S3TestBase() { } partListing.parts()[0].also { - assertThat(it.eTag()).isEqualTo("\"" + hash + "\"") + assertThat(it.eTag()).isEqualTo("\"$hash\"") assertThat(it.partNumber()).isEqualTo(1) assertThat(it.lastModified()).isExactlyInstanceOf(Instant::class.java) } partListing.parts()[1].also { - assertThat(it.eTag()).isEqualTo("\"" + hash + "\"") + assertThat(it.eTag()).isEqualTo("\"$hash\"") assertThat(it.partNumber()).isEqualTo(2) assertThat(it.lastModified()).isExactlyInstanceOf(Instant::class.java) } @@ -678,7 +678,7 @@ internal class MultipartIT : S3TestBase() { @S3VerifiedSuccess(year = 2025) fun `list parts lists uploaded parts matching parameters`(testInfo: TestInfo) { val bucketName = givenBucket(testInfo) - val objectMetadata = mapOf(Pair("key", "value")) + val objectMetadata = mapOf("key" to "value") val hash = UPLOAD_FILE.inputStream().use { DigestUtils.md5Hex(it) } val initiateMultipartUploadResult = s3Client.createMultipartUpload { it.bucket(bucketName) @@ -726,13 +726,13 @@ internal class MultipartIT : S3TestBase() { } partListing1.parts()[0].also { - assertThat(it.eTag()).isEqualTo("\"" + hash + "\"") + assertThat(it.eTag()).isEqualTo("\"$hash\"") assertThat(it.partNumber()).isEqualTo(1) assertThat(it.lastModified()).isExactlyInstanceOf(Instant::class.java) } partListing2.parts()[0].also { - assertThat(it.eTag()).isEqualTo("\"" + hash + "\"") + assertThat(it.eTag()).isEqualTo("\"$hash\"") assertThat(it.partNumber()).isEqualTo(6) assertThat(it.lastModified()).isExactlyInstanceOf(Instant::class.java) } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingIT.kt index f0858e21c..fd2cab735 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/ObjectTaggingIT.kt @@ -31,7 +31,8 @@ internal class ObjectTaggingIT : S3TestBase() { fun `GET ObjectTagging succeeds with no tags`(testInfo: TestInfo) { val key = UPLOAD_FILE_NAME val bucketName = givenBucket(testInfo) - s3Client.putObject({ + s3Client.putObject( + { it.bucket(bucketName) it.key(key) }, @@ -118,11 +119,12 @@ internal class ObjectTaggingIT : S3TestBase() { val key = UPLOAD_FILE_NAME val bucketName = givenBucket(testInfo) - s3Client.putObject({ - it.bucket(bucketName) - it.key(key) - it.tagging("msv=foo") - }, + s3Client.putObject( + { + it.bucket(bucketName) + it.key(key) + it.tagging("msv=foo") + }, RequestBody.fromString("foo") ) @@ -142,11 +144,13 @@ internal class ObjectTaggingIT : S3TestBase() { val tag1 = tag("tag1" to "foo") val tag2 = tag("tag2" to "bar") - s3Client.putObject({ + s3Client.putObject( + { it.bucket(bucketName) - it.key(key) - it.tagging(Tagging.builder().tagSet(tag1, tag2).build()) - }, RequestBody.fromString("foo") + it.key(key) + it.tagging(Tagging.builder().tagSet(tag1, tag2).build()) + }, + RequestBody.fromString("foo") ) assertThat( @@ -160,5 +164,7 @@ internal class ObjectTaggingIT : S3TestBase() { ) } - private fun tag(pair: Pair): Tag = Tag.builder().key(pair.first).value(pair.second).build() + private fun tag(key: String, value: String): Tag = Tag.builder().key(key).value(value).build() + + private fun tag(pair: Pair): Tag = tag(pair.first, pair.second) } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PlainHttpIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PlainHttpIT.kt index 8c21ddd03..4d1d86ba8 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PlainHttpIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PlainHttpIT.kt @@ -29,7 +29,6 @@ import org.apache.http.entity.ByteArrayEntity import org.apache.http.entity.ContentType import org.apache.http.entity.StringEntity import org.apache.http.impl.client.CloseableHttpClient -import org.apache.http.message.BasicHeader import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInfo @@ -37,10 +36,8 @@ import org.springframework.http.MediaType import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.utils.http.SdkHttpUtils -import java.io.File import java.io.InputStreamReader import java.util.UUID -import java.util.stream.Collectors /** * Verifies raw HTTP results for those methods where S3 Client from AWS SDK does not return anything @@ -57,8 +54,8 @@ internal class PlainHttpIT : S3TestBase() { val targetBucket = givenBucket(testInfo) val byteArray = UUID.randomUUID().toString().toByteArray() val putObject = HttpPut("$serviceEndpoint/$targetBucket/testObjectName").apply { - this.entity = ByteArrayEntity(byteArray) - this.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE) + entity = ByteArrayEntity(byteArray) + addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE) } httpClient.execute(putObject).use { @@ -73,7 +70,7 @@ internal class PlainHttpIT : S3TestBase() { val (targetBucket, _) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME) val getObject = HttpGet("$serviceEndpoint/$targetBucket/$UPLOAD_FILE_NAME").apply { - this.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE) + addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE) } httpClient.execute(getObject).use { @@ -90,9 +87,9 @@ internal class PlainHttpIT : S3TestBase() { val amzMetaHeaderKey = "x-amz-meta-my-key" val amzMetaHeaderValue = "MY_DATA" val putObject = HttpPut("$serviceEndpoint/$targetBucket/testObjectName").apply { - this.entity = ByteArrayEntity(byteArray) - this.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE) - this.addHeader(amzMetaHeaderKey, amzMetaHeaderValue) + entity = ByteArrayEntity(byteArray) + addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE) + addHeader(amzMetaHeaderKey, amzMetaHeaderValue) } httpClient.execute(putObject).use { @@ -121,8 +118,7 @@ internal class PlainHttpIT : S3TestBase() { assertThat( InputStreamReader(response.entity.content) .readLines() - .stream() - .collect(Collectors.joining())) + .joinToString(separator = "")) .isEqualTo("InvalidBucketName" + "The specified bucket is not valid.") } @@ -137,8 +133,8 @@ internal class PlainHttpIT : S3TestBase() { val targetBucket = givenBucket(testInfo) HttpPut("$serviceEndpoint/$targetBucket/testObjectName").apply { - this.addHeader("x-amz-server-side-encryption", "aws:kms") - this.entity = ByteArrayEntity(UUID.randomUUID().toString().toByteArray()) + addHeader("x-amz-server-side-encryption", "aws:kms") + entity = ByteArrayEntity(UUID.randomUUID().toString().toByteArray()) }.also { httpClient.execute(it).use { response -> assertThat(response.statusLine.statusCode).isEqualTo(HttpStatus.SC_OK) @@ -178,9 +174,9 @@ internal class PlainHttpIT : S3TestBase() { arrayOf("GET", "PUT", "HEAD").forEach { method -> val httpOptions = HttpOptions("$serviceEndpoint/$targetBucket").apply { - this.setHeader(BasicHeader("Origin", "http://someurl.com")) - this.setHeader(BasicHeader("Access-Control-Request-Method", method)) - this.setHeader(BasicHeader("Access-Control-Request-Headers", "Content-Type, x-requested-with")) + setHeader("Origin", "http://someurl.com") + setHeader("Access-Control-Request-Method", method) + setHeader("Access-Control-Request-Headers", "Content-Type, x-requested-with") } httpClient.execute(httpOptions).use { @@ -203,9 +199,9 @@ internal class PlainHttpIT : S3TestBase() { arrayOf("POST").forEach { method -> val httpOptions = HttpOptions("$serviceEndpoint/$targetBucket?delete").apply { - this.setHeader(BasicHeader("Origin", "http://someurl.com")) - this.setHeader(BasicHeader("Access-Control-Request-Method", method)) - this.setHeader(BasicHeader("Access-Control-Request-Headers", "Content-Type, x-requested-with")) + setHeader("Origin", "http://someurl.com") + setHeader("Access-Control-Request-Method", method) + setHeader("Access-Control-Request-Headers", "Content-Type, x-requested-with") } httpClient.execute(httpOptions).use { @@ -239,11 +235,11 @@ internal class PlainHttpIT : S3TestBase() { val targetBucket = givenBucket(testInfo) HttpPost("$serviceEndpoint/$targetBucket?delete").apply { - this.entity = StringEntity( + entity = StringEntity( """ myFile-1 myFile-2 - """.trimMargin(), + """, ContentType.APPLICATION_XML ) }.also { @@ -276,14 +272,14 @@ internal class PlainHttpIT : S3TestBase() { HttpPost("$serviceEndpoint/$targetBucket/$UPLOAD_FILE_NAME?uploadId=$uploadId").apply { - this.entity = StringEntity( + entity = StringEntity( """ ${uploadPartResult.eTag()} 1 - """.trimMargin(), + """, ContentType.APPLICATION_XML ) }.also { @@ -302,7 +298,7 @@ internal class PlainHttpIT : S3TestBase() { HttpPut( "$serviceEndpoint/$targetBucket/${SdkHttpUtils.urlEncodeIgnoreSlashes(fileNameWithSpecialCharacters)}" ).apply { - this.entity = ByteArrayEntity(UUID.randomUUID().toString().toByteArray()) + entity = ByteArrayEntity(UUID.randomUUID().toString().toByteArray()) }.also { httpClient.execute(it).use { response -> assertThat(response.statusLine.statusCode).isEqualTo(HttpStatus.SC_OK) @@ -338,12 +334,12 @@ internal class PlainHttpIT : S3TestBase() { val targetBucket = givenBucket(testInfo) HttpPost("$serviceEndpoint/$targetBucket?delete").apply { - this.entity = StringEntity( + entity = StringEntity( """ myFile-1 myFile-2 - """.trimMargin(), + """, ContentType.APPLICATION_XML ) }.also { diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUrlIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUrlIT.kt index 3a1b75384..7a25f7b80 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUrlIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/PresignedUrlIT.kt @@ -30,7 +30,6 @@ import org.apache.http.entity.FileEntity import org.apache.http.entity.StringEntity import org.apache.http.entity.mime.MultipartEntityBuilder import org.apache.http.impl.client.CloseableHttpClient -import org.apache.http.message.BasicHeader import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInfo @@ -222,7 +221,7 @@ internal class PresignedUrlIT : S3TestBase() { assertThat(presignedUrlString).isNotBlank() HttpPut(presignedUrlString).apply { - this.entity = FileEntity(UPLOAD_FILE) + entity = FileEntity(UPLOAD_FILE) }.also { put -> httpClient.execute( put @@ -398,8 +397,8 @@ internal class PresignedUrlIT : S3TestBase() { assertThat(presignedUrlString).isNotBlank() HttpPost(presignedUrlString).apply { - this.setHeader(BasicHeader("Content-Type", "application/xml")) - this.entity = StringEntity( + setHeader("Content-Type", "application/xml") + entity = StringEntity( """ ${uploadPartResult.eTag()} diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RealS3BackendUsedCondition.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RealS3BackendUsedCondition.kt index c4d8898cd..e385f7a18 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RealS3BackendUsedCondition.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RealS3BackendUsedCondition.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 Adobe. + * Copyright 2017-2025 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,15 +25,13 @@ import org.junit.platform.commons.util.AnnotationUtils * ExecutionCondition that evaluates if a test should be disabled. * When running integration tests, endpoint can be overwritten by setting * "it.s3mock.endpoint". - * Disable test annotated with {@link S3VerifiedFailure} when test runs against S3. + * Disable test annotated with S3VerifiedFailure when test runs against S3. */ class RealS3BackendUsedCondition : ExecutionCondition { override fun evaluateExecutionCondition(context: ExtensionContext): ConditionEvaluationResult { val failure = AnnotationUtils.findAnnotation(context.element, S3VerifiedFailure::class.java) - if (failure.isPresent) { - if (System.getProperty("it.s3mock.endpoint", null) != null) { - return ConditionEvaluationResult.disabled(failure.get().reason) - } + if (failure.isPresent && System.getProperty("it.s3mock.endpoint") != null) { + return ConditionEvaluationResult.disabled(failure.get().reason) } return ConditionEvaluationResult.enabled("") } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RetentionIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RetentionIT.kt index 2de8bb237..7e62e61f7 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RetentionIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/RetentionIT.kt @@ -23,12 +23,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInfo import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3Client -import software.amazon.awssdk.services.s3.model.CreateBucketRequest -import software.amazon.awssdk.services.s3.model.GetObjectRetentionRequest import software.amazon.awssdk.services.s3.model.ObjectLockRetention import software.amazon.awssdk.services.s3.model.ObjectLockRetentionMode -import software.amazon.awssdk.services.s3.model.PutObjectRequest -import software.amazon.awssdk.services.s3.model.PutObjectRetentionRequest import software.amazon.awssdk.services.s3.model.S3Exception import java.time.Instant import java.time.temporal.ChronoUnit.DAYS @@ -44,13 +40,10 @@ internal class RetentionIT : S3TestBase() { val (bucketName, _) = givenBucketAndObject(testInfo, sourceKey) assertThatThrownBy { - s3Client.getObjectRetention( - GetObjectRetentionRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .build() - ) + s3Client.getObjectRetention { + it.bucket(bucketName) + it.key(sourceKey) + } }.isInstanceOf(S3Exception::class.java) .hasMessageContaining("Bucket is missing Object Lock Configuration") .hasMessageContaining("Service: S3, Status Code: 400") @@ -61,30 +54,23 @@ internal class RetentionIT : S3TestBase() { fun testGetRetentionNoObjectLockConfiguration(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val bucketName = bucketName(testInfo) - s3Client.createBucket( - CreateBucketRequest - .builder() - .bucket(bucketName) - .objectLockEnabledForBucket(true) - .build() - ) + s3Client.createBucket { + it.bucket(bucketName) + it.objectLockEnabledForBucket(true) + } s3Client.putObject( - PutObjectRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .build(), + { + it.bucket(bucketName) + it.key(sourceKey) + }, RequestBody.fromFile(UPLOAD_FILE) ) assertThatThrownBy { - s3Client.getObjectRetention( - GetObjectRetentionRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .build() - ) + s3Client.getObjectRetention { + it.bucket(bucketName) + it.key(sourceKey) + } }.isInstanceOf(S3Exception::class.java) .hasMessageContaining("The specified object does not have a ObjectLock configuration") .hasMessageContaining("Service: S3, Status Code: 404") @@ -96,44 +82,34 @@ internal class RetentionIT : S3TestBase() { fun testPutAndGetRetention(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val bucketName = bucketName(testInfo) - s3Client.createBucket( - CreateBucketRequest - .builder() - .bucket(bucketName) - .objectLockEnabledForBucket(true) - .build() - ) + s3Client.createBucket { + it.bucket(bucketName) + it.objectLockEnabledForBucket(true) + } s3Client.putObject( - PutObjectRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .build(), + { + it.bucket(bucketName) + it.key(sourceKey) + }, RequestBody.fromFile(UPLOAD_FILE) ) val retainUntilDate = Instant.now().plus(1, DAYS) - s3Client.putObjectRetention( - PutObjectRetentionRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .retention( - ObjectLockRetention.builder() - .mode(ObjectLockRetentionMode.COMPLIANCE) - .retainUntilDate(retainUntilDate) - .build() - ) - .build() - ) + s3Client.putObjectRetention { + it.bucket(bucketName) + it.key(sourceKey) + it.retention( + ObjectLockRetention.builder() + .mode(ObjectLockRetentionMode.COMPLIANCE) + .retainUntilDate(retainUntilDate) + .build() + ) + } - s3Client.getObjectRetention( - GetObjectRetentionRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .build() - ).also { + s3Client.getObjectRetention { + it.bucket(bucketName) + it.key(sourceKey) + }.also { assertThat(it.retention().mode()).isEqualTo(ObjectLockRetentionMode.COMPLIANCE) //the returned date has MILLIS resolution, the local instant is in NANOS. assertThat(it.retention().retainUntilDate()) @@ -148,37 +124,30 @@ internal class RetentionIT : S3TestBase() { fun testPutInvalidRetentionUntilDate(testInfo: TestInfo) { val sourceKey = UPLOAD_FILE_NAME val bucketName = bucketName(testInfo) - s3Client.createBucket( - CreateBucketRequest - .builder() - .bucket(bucketName) - .objectLockEnabledForBucket(true) - .build() - ) + s3Client.createBucket { + it.bucket(bucketName) + it.objectLockEnabledForBucket(true) + } s3Client.putObject( - PutObjectRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .build(), + { + it.bucket(bucketName) + it.key(sourceKey) + }, RequestBody.fromFile(UPLOAD_FILE) ) val invalidRetainUntilDate = Instant.now().minus(1, DAYS) assertThatThrownBy { - s3Client.putObjectRetention( - PutObjectRetentionRequest - .builder() - .bucket(bucketName) - .key(sourceKey) - .retention( - ObjectLockRetention.builder() - .mode(ObjectLockRetentionMode.COMPLIANCE) - .retainUntilDate(invalidRetainUntilDate) - .build() - ) - .build() - ) + s3Client.putObjectRetention { + it.bucket(bucketName) + it.key(sourceKey) + it.retention( + ObjectLockRetention.builder() + .mode(ObjectLockRetentionMode.COMPLIANCE) + .retainUntilDate(invalidRetainUntilDate) + .build() + ) + } }.isInstanceOf(S3Exception::class.java) .hasMessageContaining("The retain until date must be in the future!") .hasMessageContaining("Service: S3, Status Code: 400") 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 dd36a85c9..e72e223f4 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 @@ -58,7 +58,6 @@ import software.amazon.awssdk.transfer.s3.S3TransferManager import software.amazon.awssdk.utils.AttributeMap import tel.schich.awss3postobjectpresigner.S3PostObjectPresigner import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException import java.io.InputStream @@ -227,43 +226,32 @@ internal abstract class S3TestBase { */ @AfterEach fun cleanupStores() { - for (bucket in _s3Client.listBuckets().buckets()) { - //Empty all buckets + _s3Client.listBuckets().buckets().forEach { bucket -> + // Empty all buckets deleteMultipartUploads(bucket) deleteObjectsInBucket(bucket, isObjectLockEnabled(bucket)) - //Delete all "non-initial" buckets. - if (!INITIAL_BUCKET_NAMES.contains(bucket.name())) { + // Delete all "non-initial" buckets. + if (bucket.name() !in INITIAL_BUCKET_NAMES) { deleteBucket(bucket) } } } protected fun bucketName(testInfo: TestInfo): String { - val normalizedName = testInfo.testMethod.get().name.let { - it.lowercase() - .replace('_', '-') - .replace(' ', '-') - .replace(',', '-') - .replace('\'', '-') - .replace('=', '-') - .let { - if (it.length > 50) { - //max bucket name length is 63, shorten name to 50 since we add the timestamp below. - it.substring(0, 50) - } else { - it - } - } - } + val normalizedName = testInfo.testMethod.get().name + .lowercase() + .replace('_', '-') + .replace(' ', '-') + .replace(',', '-') + .replace('\'', '-') + .replace('=', '-') + .let { if (it.length > 50) it.take(50) else it } val bucketName = "$normalizedName-${Instant.now().nano}" LOG.info("Bucketname=$bucketName") return bucketName } - fun givenBucket(testInfo: TestInfo): String { - val bucketName = bucketName(testInfo) - return givenBucket(bucketName) - } + fun givenBucket(testInfo: TestInfo): String = givenBucket(bucketName(testInfo)) fun givenBucket(bucketName: String = randomName): String { _s3Client.createBucket { it.bucket(bucketName) } @@ -290,10 +278,12 @@ internal abstract class S3TestBase { fun givenObject(bucketName: String, key: String, fileName: String? = null): PutObjectResponse { val uploadFile = File(fileName ?: key) - return _s3Client.putObject({ + return _s3Client.putObject( + { it.bucket(bucketName) it.key(key) - }, RequestBody.fromFile(uploadFile) + }, + RequestBody.fromFile(uploadFile) ) } @@ -318,12 +308,12 @@ internal abstract class S3TestBase { } fun givenBucketAndObjects(testInfo: TestInfo, count: Int): Pair> { - val keys = mutableListOf() val baseKey = randomName val bucketName = givenBucket(testInfo) - for (i in 0 until count) { - val key = "$baseKey-$i" - keys.add(key) + val keys = (0 until count).map { i -> + "$baseKey-$i" + } + keys.forEach { key -> givenObject(bucketName, key, UPLOAD_FILE_NAME) } return bucketName to keys @@ -483,17 +473,13 @@ internal abstract class S3TestBase { * Creates 5+MB of random bytes to upload as a valid part * (all parts but the last must be at least 5MB in size) */ - fun randomBytes(): ByteArray { - return randomMBytes(_5MB.toInt() + Random.nextInt(_1MB)) - } + fun randomBytes(): ByteArray = randomMBytes(_5MB.toInt() + Random.nextInt(_1MB)) /** * Creates exactly 5MB of random bytes to upload as a valid part * (all parts but the last must be at least 5MB in size) */ - fun random5MBytes(): ByteArray { - return randomMBytes(_5MB.toInt()) - } + fun random5MBytes(): ByteArray = randomMBytes(_5MB.toInt()) protected fun randomMBytes(size: Int): ByteArray { val bytes = ByteArray(size) @@ -503,22 +489,15 @@ internal abstract class S3TestBase { @Throws(IOException::class) fun readStreamIntoByteArray(inputStream: InputStream): ByteArray { - inputStream.use { it -> - val outputStream = ByteArrayOutputStream(BUFFER_SIZE) - val buffer = ByteArray(BUFFER_SIZE) - var bytesRead: Int - while (it.read(buffer).also { bytesRead = it } != -1) { - outputStream.write(buffer, 0, bytesRead) - } - outputStream.flush() - return outputStream.toByteArray() - } + // Use Kotlin's native extension for InputStream to read all bytes idiomatically + return inputStream.use { it.readBytes() } } fun concatByteArrays(arr1: ByteArray, arr2: ByteArray): ByteArray { + // Idiomatic Kotlin: allocate once and copy using copyInto to avoid System.arraycopy val result = ByteArray(arr1.size + arr2.size) - System.arraycopy(arr1, 0, result, 0, arr1.size) - System.arraycopy(arr2, 0, result, arr1.size, arr2.size) + arr1.copyInto(result, destinationOffset = 0) + arr2.copyInto(result, destinationOffset = arr1.size) return result } diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt index ba89b1de2..34cc226be 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/VersionsIT.kt @@ -95,9 +95,8 @@ internal class VersionsIT : S3TestBase() { ObjectAttributes.CHECKSUM ) }.also { - // assertThat(it.versionId()).isEqualTo(versionId) - //default storageClass is STANDARD, which is never returned from APIs + // default storageClass is STANDARD, which is never returned from APIs assertThat(it.storageClass()).isEqualTo(StorageClass.STANDARD) assertThat(it.objectSize()).isEqualTo(UPLOAD_FILE_LENGTH) assertThat(it.checksum().checksumSHA1()).isEqualTo(expectedChecksum) 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 e6a3f76bb..87ec7d075 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 @@ -241,7 +241,7 @@ public BucketLifecycleConfiguration getBucketLifecycleConfiguration(String bucke public List getS3Objects(String bucketName, @Nullable String prefix) { var bucketMetadata = bucketStore.getBucketMetadata(bucketName); - var uuids = bucketStore.lookupKeysInBucket(prefix, bucketName); + var uuids = bucketStore.lookupIdsInBucket(prefix, bucketName); return uuids .stream() .filter(Objects::nonNull) 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 035b46d80..a78d25e4d 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 @@ -36,6 +36,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import org.apache.commons.io.FileUtils; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; @@ -101,7 +102,19 @@ public synchronized UUID addKeyToBucket(String key, String bucketName) { } } - public List lookupKeysInBucket(@Nullable String prefix, String bucketName) { + public List lookupIdsInBucket(@Nullable String prefix, String bucketName) { + return lookupInBucket(prefix, bucketName, Map.Entry::getValue); + } + + public List lookupKeysInBucket(@Nullable String prefix, String bucketName) { + return lookupInBucket(prefix, bucketName, Map.Entry::getKey); + } + + private List lookupInBucket( + @Nullable String prefix, + String bucketName, + Function, R> extract + ) { var bucketMetadata = getBucketMetadata(bucketName); var normalizedPrefix = prefix == null ? "" : prefix; synchronized (lockStore.get(bucketName)) { @@ -109,7 +122,7 @@ public List lookupKeysInBucket(@Nullable String prefix, String bucketName) .entrySet() .stream() .filter(entry -> entry.getKey().startsWith(normalizedPrefix)) - .map(Map.Entry::getValue) + .map(extract) .toList(); } } 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 c1a7016df..2ae19c79a 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 @@ -27,9 +27,8 @@ 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 static org.apache.commons.lang3.Strings.CI; import com.adobe.testing.s3mock.dto.ChecksumAlgorithm; import com.adobe.testing.s3mock.dto.StorageClass; @@ -73,7 +72,7 @@ public static Map userMetadataHeadersFrom(S3ObjectMetadata s3Obj if (s3ObjectMetadata.userMetadata() != null) { s3ObjectMetadata.userMetadata() .forEach((key, value) -> { - if (startsWithIgnoreCase(key, HEADER_X_AMZ_META_PREFIX)) { + if (CI.startsWith(key, HEADER_X_AMZ_META_PREFIX)) { metadataHeaders.put(key, value); } else { //support case where metadata was stored locally in legacy format @@ -104,7 +103,7 @@ public static Map storageClassHeadersFrom(S3ObjectMetadata s3Obj */ public static Map userMetadataFrom(HttpHeaders headers) { return parseHeadersToMap(headers, - header -> startsWithIgnoreCase(header, HEADER_X_AMZ_META_PREFIX)); + header -> CI.startsWith(header, HEADER_X_AMZ_META_PREFIX)); } /** @@ -114,12 +113,12 @@ public static Map userMetadataFrom(HttpHeaders headers) { */ public static Map storeHeadersFrom(HttpHeaders headers) { return parseHeadersToMap(headers, - header -> (equalsIgnoreCase(header, HttpHeaders.EXPIRES) - || equalsIgnoreCase(header, HttpHeaders.CONTENT_LANGUAGE) - || equalsIgnoreCase(header, HttpHeaders.CONTENT_DISPOSITION) - || (equalsIgnoreCase(header, HttpHeaders.CONTENT_ENCODING) + header -> (CI.equals(header, HttpHeaders.EXPIRES) + || CI.equals(header, HttpHeaders.CONTENT_LANGUAGE) + || CI.equals(header, HttpHeaders.CONTENT_DISPOSITION) + || (CI.equals(header, HttpHeaders.CONTENT_ENCODING) && !isOnlyChunkedEncoding(headers)) - || equalsIgnoreCase(header, HttpHeaders.CACHE_CONTROL) + || CI.equals(header, HttpHeaders.CACHE_CONTROL) )); } @@ -130,7 +129,7 @@ public static Map storeHeadersFrom(HttpHeaders headers) { */ public static Map encryptionHeadersFrom(HttpHeaders headers) { return parseHeadersToMap(headers, - header -> startsWithIgnoreCase(header, X_AMZ_SERVER_SIDE_ENCRYPTION)); + header -> CI.startsWith(header, X_AMZ_SERVER_SIDE_ENCRYPTION)); } private static Map parseHeadersToMap(HttpHeaders headers, diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/BaseControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/BaseControllerTest.kt index ca88427c6..40aa58bf4 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/BaseControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/BaseControllerTest.kt @@ -15,11 +15,29 @@ */ package com.adobe.testing.s3mock +import com.adobe.testing.s3mock.dto.Bucket +import com.adobe.testing.s3mock.dto.BucketInfo +import com.adobe.testing.s3mock.dto.ChecksumAlgorithm +import com.adobe.testing.s3mock.dto.ChecksumType +import com.adobe.testing.s3mock.dto.ErrorResponse +import com.adobe.testing.s3mock.dto.LegalHold +import com.adobe.testing.s3mock.dto.LocationInfo +import com.adobe.testing.s3mock.dto.Owner +import com.adobe.testing.s3mock.dto.Retention +import com.adobe.testing.s3mock.dto.Tag +import com.adobe.testing.s3mock.dto.VersioningConfiguration +import com.adobe.testing.s3mock.store.BucketMetadata +import com.adobe.testing.s3mock.store.S3ObjectMetadata +import com.adobe.testing.s3mock.util.AwsHttpHeaders import com.ctc.wstx.api.WstxOutputProperties import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.dataformat.xml.XmlMapper import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator +import java.nio.file.Path +import java.nio.file.Paths +import java.time.Instant +import java.util.UUID internal abstract class BaseControllerTest { companion object { @@ -35,5 +53,110 @@ internal abstract class BaseControllerTest { MAPPER.factory.xmlOutputFactory .setProperty(WstxOutputProperties.P_USE_DOUBLE_QUOTES_IN_XML_DECL, true) } + + fun from(e: S3Exception): ErrorResponse = ErrorResponse( + e.code, + e.message, + null, + null + ) + + fun bucketMetadata( + name: String = TEST_BUCKET_NAME, + creationDate: String = Instant.now().toString(), + path: Path = Paths.get("/tmp/foo/1"), + bucketRegion: String = "us-east-1", + versioningConfiguration: VersioningConfiguration? = null, + bucketInfo: BucketInfo? = null, + locationInfo: LocationInfo? = null + ): BucketMetadata { + return BucketMetadata( + name, + creationDate, + versioningConfiguration, + null, + null, + null, + path, + bucketRegion, + bucketInfo, + locationInfo, + ) + } + + fun s3ObjectEncrypted( + key: String, + digest: String = UUID.randomUUID().toString(), + encryption: String?, + encryptionKey: String? + ): S3ObjectMetadata { + return s3ObjectMetadata( + key, digest, encryption, encryptionKey, + ) + } + + fun s3ObjectMetadata( + key: String, + digest: String = UUID.randomUUID().toString(), + encryption: String? = null, + encryptionKey: String? = null, + retention: Retention? = null, + tags: List? = null, + legalHold: LegalHold? = null, + versionId: String? = null, + checksum: String? = null, + checksumType: ChecksumType? = ChecksumType.FULL_OBJECT, + checksumAlgorithm: ChecksumAlgorithm? = null, + userMetadata: Map? = null, + storeHeaders: Map? = null, + ): S3ObjectMetadata { + return S3ObjectMetadata( + UUID.randomUUID(), + key, + Path.of(UPLOAD_FILE_NAME).toFile().length().toString(), + "1234", + digest, + "text/plain", + 1L, + Path.of(UPLOAD_FILE_NAME), + userMetadata, + tags, + legalHold, + retention, + Owner.DEFAULT_OWNER, + storeHeaders, + encryptionHeaders(encryption, encryptionKey), + checksumAlgorithm, + checksum, + null, + null, + versionId, + false, + checksumType + ) + } + + private fun encryptionHeaders(encryption: String?, encryptionKey: String?): Map = buildMap { + if (encryption != null) { + put(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION, encryption) + } + if (encryptionKey != null) { + put(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, encryptionKey) + } + } + val TEST_OWNER = Owner("s3-mock-file-store", "123") + val TEST_BUCKETMETADATA = bucketMetadata() + const val UPLOAD_FILE_NAME = "src/test/resources/sampleFile.txt" + + const val TEST_BUCKET_NAME = "test-bucket" + val CREATION_DATE = Instant.now().toString() + const val BUCKET_REGION = "us-west-2" + val BUCKET_PATH: Path = Paths.get("/tmp/foo/1") + val TEST_BUCKET = Bucket( + TEST_BUCKET_NAME, + BUCKET_REGION, + CREATION_DATE, + BUCKET_PATH + ) } } 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 16e28873f..25b4f69bf 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/BucketControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/BucketControllerTest.kt @@ -25,7 +25,6 @@ import com.adobe.testing.s3mock.dto.ChecksumType import com.adobe.testing.s3mock.dto.CreateBucketConfiguration import com.adobe.testing.s3mock.dto.DataRedundancy.SINGLE_AVAILABILITY_ZONE import com.adobe.testing.s3mock.dto.DefaultRetention -import com.adobe.testing.s3mock.dto.ErrorResponse import com.adobe.testing.s3mock.dto.LifecycleExpiration import com.adobe.testing.s3mock.dto.LifecycleRule import com.adobe.testing.s3mock.dto.LifecycleRuleFilter @@ -33,7 +32,6 @@ import com.adobe.testing.s3mock.dto.ListAllMyBucketsResult import com.adobe.testing.s3mock.dto.ListBucketResult import com.adobe.testing.s3mock.dto.ListBucketResultV2 import com.adobe.testing.s3mock.dto.ListVersionsResult -import com.adobe.testing.s3mock.dto.VersioningConfiguration import com.adobe.testing.s3mock.dto.LocationConstraint import com.adobe.testing.s3mock.dto.LocationInfo import com.adobe.testing.s3mock.dto.LocationType.AVAILABILITY_ZONE @@ -42,11 +40,11 @@ import com.adobe.testing.s3mock.dto.ObjectLockConfiguration import com.adobe.testing.s3mock.dto.ObjectLockEnabled import com.adobe.testing.s3mock.dto.ObjectLockRule import com.adobe.testing.s3mock.dto.ObjectOwnership.BUCKET_OWNER_ENFORCED -import com.adobe.testing.s3mock.dto.Owner import com.adobe.testing.s3mock.dto.Region import com.adobe.testing.s3mock.dto.S3Object import com.adobe.testing.s3mock.dto.StorageClass import com.adobe.testing.s3mock.dto.Transition +import com.adobe.testing.s3mock.dto.VersioningConfiguration import com.adobe.testing.s3mock.service.BucketService import com.adobe.testing.s3mock.service.MultipartService import com.adobe.testing.s3mock.service.ObjectService @@ -58,116 +56,92 @@ import com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_REGION import com.adobe.testing.s3mock.util.AwsHttpParameters import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString import org.mockito.kotlin.doThrow -import org.mockito.kotlin.verify import org.mockito.kotlin.eq +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod -import org.springframework.http.HttpStatus +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder -import java.net.URI import java.nio.file.Paths import java.time.Instant @MockitoBean(types = [KmsKeyStore::class, ObjectService::class, MultipartService::class, ObjectController::class, MultipartController::class]) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = ["com.adobe.testing.s3mock.region=us-east-1"]) +@WebMvcTest(properties = ["com.adobe.testing.s3mock.region=us-east-1"]) internal class BucketControllerTest : BaseControllerTest() { @MockitoBean private lateinit var bucketService: BucketService @Autowired - private lateinit var restTemplate: TestRestTemplate + private lateinit var mockMvc: MockMvc @Test fun `HEAD bucket returns OK if bucket exists`() { givenBucket() - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.HEAD, - HttpEntity(headers), - String::class.java - ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + mockMvc.perform( + head("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML)) + .andExpect(status().isOk) + .andReturn() } @Test fun `HEAD bucket returns bucketInfo and locationInfo headers if available`() { whenever(bucketService.bucketLocationHeaders(any(BucketMetadata::class.java))).thenCallRealMethod() givenBucket(bucketMetadata( - BucketInfo(SINGLE_AVAILABILITY_ZONE, DIRECTORY), - LocationInfo("SomeName", AVAILABILITY_ZONE) + bucketRegion = BUCKET_REGION, + bucketInfo = BucketInfo(SINGLE_AVAILABILITY_ZONE, DIRECTORY), + locationInfo = LocationInfo("SomeName", AVAILABILITY_ZONE) )) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.HEAD, - HttpEntity(headers), - String::class.java - ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers[X_AMZ_BUCKET_LOCATION_TYPE]).isEqualTo(listOf(AVAILABILITY_ZONE.toString())) - assertThat(response.headers[X_AMZ_BUCKET_LOCATION_NAME]).isEqualTo(listOf("SomeName")) - assertThat(response.headers[X_AMZ_BUCKET_REGION]).isEqualTo(listOf(BUCKET_REGION)) + mockMvc.perform( + head("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML)) + .andExpect(status().isOk) + .andExpect(header().stringValues(X_AMZ_BUCKET_LOCATION_TYPE, AVAILABILITY_ZONE.toString())) + .andExpect(header().stringValues(X_AMZ_BUCKET_LOCATION_NAME, "SomeName")) + .andExpect(header().stringValues(X_AMZ_BUCKET_REGION, BUCKET_REGION)) } @Test fun `HEAD bucket for non-existing bucket returns 404`() { doThrow(S3Exception.NO_SUCH_BUCKET).whenever(bucketService) - .verifyBucketExists(ArgumentMatchers.anyString()) + .verifyBucketExists(anyString()) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.GET, - HttpEntity(headers), - String::class.java - ) - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) + mockMvc.perform( + get("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML)) + .andExpect(status().isNotFound) } @Test fun `creating a bucket without configuration returns OK and location`() { - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/${TEST_BUCKET_NAME}", - HttpMethod.PUT, - HttpEntity(headers), - String::class.java - ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.location).isEqualTo(URI.create("/${TEST_BUCKET_NAME}")) + mockMvc.perform( + put("/${TEST_BUCKET_NAME}") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML)) + .andExpect(status().isOk) + .andExpect(header().string("Location", "/${TEST_BUCKET_NAME}")) + verify(bucketService).createBucket(TEST_BUCKET_NAME, false, BUCKET_OWNER_ENFORCED, null, null, null) } @Test fun `PUT bucket with configuration returns OK and location`() { - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val bucketInfo = BucketInfo(SINGLE_AVAILABILITY_ZONE, DIRECTORY) val locationInfo = LocationInfo("SomeName", AVAILABILITY_ZONE) val createBucketConfiguration = CreateBucketConfiguration( @@ -176,14 +150,15 @@ internal class BucketControllerTest : BaseControllerTest() { LocationConstraint(BUCKET_REGION), ) - val response = restTemplate.exchange( - "/${TEST_BUCKET_NAME}", - HttpMethod.PUT, - HttpEntity(createBucketConfiguration, headers), - String::class.java + mockMvc.perform( + put("/${TEST_BUCKET_NAME}") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(createBucketConfiguration)) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.location).isEqualTo(URI.create("/${TEST_BUCKET_NAME}")) + .andExpect(status().isOk) + .andExpect(header().string("Location", "/${TEST_BUCKET_NAME}")) + verify(bucketService).createBucket(TEST_BUCKET_NAME, false, BUCKET_OWNER_ENFORCED, BUCKET_REGION, bucketInfo, locationInfo) } @@ -192,17 +167,12 @@ internal class BucketControllerTest : BaseControllerTest() { whenever(bucketService.createBucket(TEST_BUCKET_NAME, false, BUCKET_OWNER_ENFORCED, null, null, null)) .thenThrow(IllegalStateException("THIS IS EXPECTED")) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.PUT, - HttpEntity(headers), - String::class.java + mockMvc.perform( + put("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .andExpect(status().isInternalServerError) } @Test @@ -212,37 +182,27 @@ internal class BucketControllerTest : BaseControllerTest() { whenever(bucketService.isBucketEmpty(TEST_BUCKET_NAME)).thenReturn(true) whenever(bucketService.deleteBucket(TEST_BUCKET_NAME)).thenReturn(true) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.DELETE, - HttpEntity(headers), - String::class.java + mockMvc.perform( + delete("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.NO_CONTENT) + .andExpect(status().isNoContent) } @Test @Throws(Exception::class) fun testDeleteBucket_NotFound() { doThrow(S3Exception.NO_SUCH_BUCKET) - .whenever(bucketService).verifyBucketIsEmpty(ArgumentMatchers.anyString()) + .whenever(bucketService).verifyBucketIsEmpty(anyString()) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.DELETE, - HttpEntity(headers), - String::class.java + mockMvc.perform( + delete("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test @@ -250,7 +210,7 @@ internal class BucketControllerTest : BaseControllerTest() { fun testDeleteBucket_Conflict() { givenBucket() doThrow(S3Exception.BUCKET_NOT_EMPTY) - .whenever(bucketService).verifyBucketIsEmpty(ArgumentMatchers.anyString()) + .whenever(bucketService).verifyBucketIsEmpty(anyString()) whenever(bucketService.getS3Objects(TEST_BUCKET_NAME, null)) .thenReturn( @@ -261,18 +221,13 @@ internal class BucketControllerTest : BaseControllerTest() { ) ) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.DELETE, - HttpEntity(headers), - String::class.java + mockMvc.perform( + delete("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.CONFLICT) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.BUCKET_NOT_EMPTY))) + .andExpect(status().isConflict) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.BUCKET_NOT_EMPTY)))) } @Test @@ -280,19 +235,14 @@ internal class BucketControllerTest : BaseControllerTest() { givenBucket() doThrow(IllegalStateException("THIS IS EXPECTED")) - .whenever(bucketService).verifyBucketIsEmpty(ArgumentMatchers.anyString()) + .whenever(bucketService).verifyBucketIsEmpty(anyString()) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.DELETE, - HttpEntity(headers), - String::class.java + mockMvc.perform( + delete("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .andExpect(status().isInternalServerError) } @Test @@ -300,18 +250,13 @@ internal class BucketControllerTest : BaseControllerTest() { fun `GET list buckets returns all buckets if no parameters are given`() { val expected = givenBuckets(2) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/", - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get("/") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -336,18 +281,14 @@ internal class BucketControllerTest : BaseControllerTest() { .queryParam(AwsHttpParameters.MAX_BUCKETS, maxBuckets.toString()) .build() .toString() - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -355,18 +296,13 @@ internal class BucketControllerTest : BaseControllerTest() { fun `GET list buckets result is empty if no buckets exist`() { val expected = givenBuckets(0) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/", - HttpMethod.GET, - HttpEntity(headers), - String::class.java - ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + mockMvc.perform( + get("/") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + ) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) assertThat(expected.buckets.buckets).isEmpty() } @@ -380,38 +316,33 @@ internal class BucketControllerTest : BaseControllerTest() { val encodingtype = "not_valid" doThrow(S3Exception.INVALID_REQUEST_ENCODING_TYPE).whenever(bucketService).verifyEncodingType(encodingtype) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val maxKeysUri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.MAX_KEYS, maxKeys.toString()) .build() .toString() - val maxKeysResponse = restTemplate.exchange( - maxKeysUri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(maxKeysUri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(maxKeysResponse.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(maxKeysResponse.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_MAX_KEYS))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_MAX_KEYS)))) val encodingTypeUri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.ENCODING_TYPE, encodingtype) .build() .toString() - val encodingTypeResponse = restTemplate.exchange( - encodingTypeUri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(encodingTypeUri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(encodingTypeResponse.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(encodingTypeResponse.body) - .isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_ENCODING_TYPE))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_ENCODING_TYPE)))) } @Test @@ -424,39 +355,33 @@ internal class BucketControllerTest : BaseControllerTest() { val encodingtype = "not_valid" doThrow(S3Exception.INVALID_REQUEST_ENCODING_TYPE).whenever(bucketService).verifyEncodingType(encodingtype) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val maxKeysUri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam("list-type", "2") .queryParam(AwsHttpParameters.MAX_KEYS, maxKeys.toString()) .build().toString() - val maxKeysResponse = restTemplate.exchange( - maxKeysUri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(maxKeysUri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(maxKeysResponse.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(maxKeysResponse.body) - .isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_MAX_KEYS))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_MAX_KEYS)))) val encodingTypeUri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.ENCODING_TYPE, encodingtype) .queryParam("list-type", "2") .build().toString() - val encodingTypeResponse = restTemplate.exchange( - encodingTypeUri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(encodingTypeUri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(encodingTypeResponse.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(encodingTypeResponse.body) - .isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_ENCODING_TYPE))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_REQUEST_ENCODING_TYPE)))) } @Test @@ -474,17 +399,12 @@ internal class BucketControllerTest : BaseControllerTest() { ) ).thenThrow(IllegalStateException("THIS IS EXPECTED")) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket/", - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get("/test-bucket/") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .andExpect(status().isInternalServerError) } @Test @@ -504,22 +424,18 @@ internal class BucketControllerTest : BaseControllerTest() { ) ).thenThrow(IllegalStateException("THIS IS EXPECTED")) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/") .queryParam("list-type", "2") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + .andExpect(status().isInternalServerError) } @Test @@ -553,18 +469,13 @@ internal class BucketControllerTest : BaseControllerTest() { ) ).thenReturn(expected) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/test-bucket", - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get("/test-bucket") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -602,23 +513,19 @@ internal class BucketControllerTest : BaseControllerTest() { ) ).thenReturn(expected) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam("list-type", "2") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -629,22 +536,19 @@ internal class BucketControllerTest : BaseControllerTest() { val rule = ObjectLockRule(retention) val expected = ObjectLockConfiguration(ObjectLockEnabled.ENABLED, rule) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.OBJECT_LOCK, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity(MAPPER.writeValueAsString(expected), headers), - String::class.java + + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(expected)) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + .andExpect(status().isOk) verify(bucketService).setObjectLockConfiguration(TEST_BUCKET_NAME, expected) } @@ -659,23 +563,19 @@ internal class BucketControllerTest : BaseControllerTest() { whenever(bucketService.getObjectLockConfiguration(TEST_BUCKET_NAME)).thenReturn(expected) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.OBJECT_LOCK, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -697,22 +597,19 @@ internal class BucketControllerTest : BaseControllerTest() { ) val configuration = BucketLifecycleConfiguration(listOf(rule1, rule2)) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.LIFECYCLE, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity(MAPPER.writeValueAsString(configuration), headers), - String::class.java + + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(configuration)) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + .andExpect(status().isOk) verify(bucketService).setBucketLifecycleConfiguration(TEST_BUCKET_NAME, configuration) } @@ -738,23 +635,19 @@ internal class BucketControllerTest : BaseControllerTest() { whenever(bucketService.getBucketLifecycleConfiguration(TEST_BUCKET_NAME)).thenReturn(configuration) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.LIFECYCLE, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(configuration)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(configuration))) } @Test @@ -762,49 +655,39 @@ internal class BucketControllerTest : BaseControllerTest() { fun testDeleteBucketLifecycleConfiguration_NoContent() { givenBucket() - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.LIFECYCLE, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.DELETE, - HttpEntity(headers), - String::class.java + mockMvc.perform( + delete(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.NO_CONTENT) + .andExpect(status().isNoContent) verify(bucketService).deleteBucketLifecycleConfiguration(TEST_BUCKET_NAME) } @Test @Throws(Exception::class) fun testGetBucketLocation_Ok() { - givenBucket() + givenBucket(bucketMetadata(bucketRegion = BUCKET_REGION)) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.LOCATION, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(LocationConstraint("us-west-2"))) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(LocationConstraint("us-west-2")))) } @Test @@ -815,23 +698,19 @@ internal class BucketControllerTest : BaseControllerTest() { whenever(bucketService.getVersioningConfiguration(TEST_BUCKET_NAME)).thenReturn(expected) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.VERSIONING, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -840,22 +719,19 @@ internal class BucketControllerTest : BaseControllerTest() { givenBucket() val configuration = VersioningConfiguration(VersioningConfiguration.MFADelete.DISABLED, VersioningConfiguration.Status.SUSPENDED, null) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.VERSIONING, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity(MAPPER.writeValueAsString(configuration), headers), - String::class.java + + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(configuration)) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + .andExpect(status().isOk) verify(bucketService).setVersioningConfiguration(TEST_BUCKET_NAME, configuration) } @@ -892,24 +768,19 @@ internal class BucketControllerTest : BaseControllerTest() { ) ).thenReturn(expected) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.VERSIONS, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @@ -967,45 +838,8 @@ internal class BucketControllerTest : BaseControllerTest() { whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMetadata) } - private fun from(e: S3Exception): ErrorResponse { - return ErrorResponse( - e.code, - e.message, - null, - null - ) - } - - private fun bucketMetadata(bucketInfo: BucketInfo? = null, - locationInfo: LocationInfo? = null): BucketMetadata { - return BucketMetadata( - TEST_BUCKET_NAME, - CREATION_DATE, - null, - null, - null, - null, - BUCKET_PATH, - BUCKET_REGION, - bucketInfo, - locationInfo, - ) - } - companion object { - private val TEST_OWNER = Owner("s3-mock-file-store", "123") - private const val TEST_BUCKET_NAME = "test-bucket" - private val CREATION_DATE = Instant.now().toString() - private const val BUCKET_REGION = "us-west-2" - private val BUCKET_PATH = Paths.get("/tmp/foo/1") private const val MAX_BUCKETS_DEFAULT = 1000 private const val MAX_KEYS_DEFAULT = 1000 - - private val TEST_BUCKET = Bucket( - TEST_BUCKET_NAME, - BUCKET_REGION, - CREATION_DATE, - BUCKET_PATH - ) } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/ContextPathObjectStoreControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/ContextPathObjectStoreControllerTest.kt index 89833a3be..9fe216c24 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/ContextPathObjectStoreControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/ContextPathObjectStoreControllerTest.kt @@ -18,38 +18,32 @@ package com.adobe.testing.s3mock import com.adobe.testing.s3mock.dto.Bucket import com.adobe.testing.s3mock.dto.Buckets import com.adobe.testing.s3mock.dto.ListAllMyBucketsResult -import com.adobe.testing.s3mock.dto.Owner import com.adobe.testing.s3mock.service.BucketService import com.adobe.testing.s3mock.service.MultipartService import com.adobe.testing.s3mock.service.ObjectService import com.adobe.testing.s3mock.store.KmsKeyStore import com.adobe.testing.s3mock.store.MultipartStore -import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod -import org.springframework.http.HttpStatus +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.nio.file.Paths import java.time.Instant @MockitoBean(types = [KmsKeyStore::class, ObjectService::class, MultipartService::class, MultipartStore::class]) -@SpringBootTest( - properties = ["com.adobe.testing.s3mock.contextPath=s3-mock"], - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT -) +@WebMvcTest(properties = ["com.adobe.testing.s3mock.contextPath=s3-mock"]) internal class ContextPathObjectStoreControllerTest : BaseControllerTest() { @MockitoBean private lateinit var bucketService: BucketService @Autowired - private lateinit var restTemplate: TestRestTemplate + private lateinit var mockMvc: MockMvc @Test @Throws(Exception::class) @@ -69,26 +63,13 @@ internal class ContextPathObjectStoreControllerTest : BaseControllerTest() { ) ).thenReturn(expected) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val response = restTemplate.exchange( - "/s3-mock/", - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get("/s3-mock/") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) - } - - companion object { - private val TEST_OWNER = Owner("s3-mock-file-store", "123") - - private const val TEST_BUCKET_NAME = "testBucket" - private val TEST_BUCKET = Bucket(TEST_BUCKET_NAME, "us-east-1", Instant.now().toString(), Paths.get("/tmp/foo/1")) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/FaviconControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/FaviconControllerTest.kt index e8728a9fe..ddec517c5 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/FaviconControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/FaviconControllerTest.kt @@ -16,35 +16,27 @@ package com.adobe.testing.s3mock import com.adobe.testing.s3mock.store.KmsKeyStore -import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod -import org.springframework.http.HttpStatus +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType import org.springframework.test.context.bean.override.mockito.MockitoBean +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @MockitoBean(types = [KmsKeyStore::class, ObjectController::class, BucketController::class, MultipartController::class]) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@WebMvcTest internal class FaviconControllerTest : BaseControllerTest() { @Autowired - private lateinit var restTemplate: TestRestTemplate + private lateinit var mockMvc: MockMvc @Test fun testFavicon() { - val headers = HttpHeaders() - headers.accept = listOf(MediaType.APPLICATION_JSON) - val response = restTemplate.exchange( - "/favicon.ico", - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get("/favicon.ico") + .accept(MediaType.ALL) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + .andExpect(status().isOk) } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/HttpRangeHeaderConverterTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/HttpRangeHeaderConverterTest.kt index 50b8593fe..4be932288 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/HttpRangeHeaderConverterTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/HttpRangeHeaderConverterTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 Adobe. + * Copyright 2017-2025 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ internal class HttpRangeHeaderConverterTest { val rangeHeader = "bytes=1-2" val actual = iut.convert(rangeHeader) assertThat(actual).isNotNull() - assertThat(actual!!.getRangeStart(Long.MAX_VALUE)).isEqualTo(1) + assertThat(requireNotNull(actual).getRangeStart(Long.MAX_VALUE)).isEqualTo(1) assertThat(actual.getRangeEnd(Long.MAX_VALUE)).isEqualTo(2) } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/MultipartControllerTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/MultipartControllerTest.kt index 1a900e018..8ab01a9b9 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/MultipartControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/MultipartControllerTest.kt @@ -15,14 +15,12 @@ */ package com.adobe.testing.s3mock -import com.adobe.testing.s3mock.dto.Bucket import com.adobe.testing.s3mock.dto.ChecksumAlgorithm import com.adobe.testing.s3mock.dto.ChecksumType import com.adobe.testing.s3mock.dto.CompleteMultipartUpload import com.adobe.testing.s3mock.dto.CompleteMultipartUploadResult import com.adobe.testing.s3mock.dto.CompletedPart import com.adobe.testing.s3mock.dto.CopyPartResult -import com.adobe.testing.s3mock.dto.ErrorResponse import com.adobe.testing.s3mock.dto.InitiateMultipartUploadResult import com.adobe.testing.s3mock.dto.ListMultipartUploadsResult import com.adobe.testing.s3mock.dto.ListPartsResult @@ -35,38 +33,37 @@ import com.adobe.testing.s3mock.dto.VersioningConfiguration import com.adobe.testing.s3mock.service.BucketService import com.adobe.testing.s3mock.service.MultipartService import com.adobe.testing.s3mock.service.ObjectService -import com.adobe.testing.s3mock.store.BucketMetadata import com.adobe.testing.s3mock.store.KmsKeyStore import com.adobe.testing.s3mock.store.MultipartUploadInfo -import com.adobe.testing.s3mock.store.S3ObjectMetadata import org.apache.commons.lang3.tuple.Pair -import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.anyList import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.startsWith import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doThrow import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.http.HttpEntity +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod -import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.context.bean.override.mockito.MockitoBean -import org.springframework.util.MultiValueMap +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder -import java.nio.file.Paths -import java.time.Instant import java.util.Date import java.util.UUID @MockitoBean(types = [KmsKeyStore::class, ObjectController::class, BucketController::class]) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@WebMvcTest internal class MultipartControllerTest : BaseControllerTest() { @MockitoBean private lateinit var bucketService: BucketService @@ -78,7 +75,7 @@ internal class MultipartControllerTest : BaseControllerTest() { private lateinit var objectService: ObjectService @Autowired - private lateinit var restTemplate: TestRestTemplate + private lateinit var mockMvc: MockMvc @Test @Throws(Exception::class) @@ -115,23 +112,19 @@ internal class MultipartControllerTest : BaseControllerTest() { anyList() ) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam("uploadId", uploadId) .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.ENTITY_TOO_SMALL))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.ENTITY_TOO_SMALL)))) } @Test @@ -171,23 +164,19 @@ internal class MultipartControllerTest : BaseControllerTest() { val key = "sampleFile.txt" - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam("uploadId", uploadId) .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART)))) } @Test @@ -223,23 +212,19 @@ internal class MultipartControllerTest : BaseControllerTest() { ) } - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam("uploadId", uploadId) .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART)))) } @Test @@ -279,29 +264,24 @@ internal class MultipartControllerTest : BaseControllerTest() { ) } - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam("uploadId", uploadId) .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART_ORDER))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART_ORDER)))) } @Test fun testCompleteMultipart_Ok_EncryptionHeadersEchoed() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val key = "enc/key.txt" @@ -316,11 +296,6 @@ internal class MultipartControllerTest : BaseControllerTest() { val s3meta = s3ObjectMetadata(key, UUID.randomUUID().toString()) whenever(objectService.getObject(TEST_BUCKET_NAME, key, null)).thenReturn(s3meta) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - // create result with encryption headers to be echoed val mpUpload = MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) val info = MultipartUploadInfo( @@ -368,21 +343,26 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.getFirst("x-amz-server-side-encryption")).isEqualTo("AES256") - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(header().string("x-amz-server-side-encryption", "AES256")) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testCompleteMultipart_Ok_VersionIdHeaderWhenVersioned() { - val bucketMeta = bucketMetadata(versioningEnabled = true) + val versioningConfiguration = VersioningConfiguration( + VersioningConfiguration.MFADelete.DISABLED, + VersioningConfiguration.Status.ENABLED, + null + ) + val bucketMeta = bucketMetadata(versioningConfiguration = versioningConfiguration) + whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val key = "ver/key.txt" @@ -394,11 +374,6 @@ internal class MultipartControllerTest : BaseControllerTest() { val s3meta = s3ObjectMetadata(key, UUID.randomUUID().toString()) whenever(objectService.getObject(TEST_BUCKET_NAME, key, null)).thenReturn(s3meta) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val mpUpload = MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) val info = MultipartUploadInfo( mpUpload, @@ -438,21 +413,20 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.getFirst("x-amz-version-id")).isEqualTo("v1") - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(header().string("x-amz-version-id", "v1")) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testCompleteMultipart_Ok_NoVersionHeaderWhenNotVersioned() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val key = "nover/key.txt" @@ -464,11 +438,6 @@ internal class MultipartControllerTest : BaseControllerTest() { val s3meta = s3ObjectMetadata(key, UUID.randomUUID().toString()) whenever(objectService.getObject(TEST_BUCKET_NAME, key, null)).thenReturn(s3meta) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val mpUpload = MultipartUpload(null, null, Date(), Owner.DEFAULT_OWNER, key, Owner.DEFAULT_OWNER, StorageClass.STANDARD, uploadId.toString()) val info = MultipartUploadInfo( mpUpload, @@ -508,21 +477,20 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.getFirst("x-amz-version-id")).isNull() - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(header().doesNotExist("x-amz-version-id")) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testCompleteMultipart_PreconditionFailed() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val key = "pre/key.txt" @@ -539,27 +507,21 @@ internal class MultipartControllerTest : BaseControllerTest() { .whenever(objectService) .verifyObjectMatching(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), eq(s3meta)) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - add("If-Match", "non-matching-etag") - } - val uri = UriComponentsBuilder .fromUriString("/${TEST_BUCKET_NAME}/$key") .queryParam("uploadId", uploadId) .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .header("If-Match", "non-matching-etag") + .content(MAPPER.writeValueAsString(uploadRequest)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.PRECONDITION_FAILED) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.PRECONDITION_FAILED))) + .andExpect(status().isPreconditionFailed) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.PRECONDITION_FAILED)))) } @Test @@ -574,31 +536,25 @@ internal class MultipartControllerTest : BaseControllerTest() { val uploadRequest = CompleteMultipartUpload(ArrayList()) uploadRequest.addPart(CompletedPart(null, null, null, null, null, "etag1", 1)) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val uri = UriComponentsBuilder .fromUriString("/${TEST_BUCKET_NAME}/$key") .queryParam("uploadId", uploadId) .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test fun testCompleteMultipart_NoSuchUpload() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val key = "no-upload/key.txt" @@ -611,32 +567,26 @@ internal class MultipartControllerTest : BaseControllerTest() { val uploadRequest = CompleteMultipartUpload(ArrayList()) uploadRequest.addPart(CompletedPart(null, null, null, null, null, "etag1", 1)) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } - val uri = UriComponentsBuilder .fromUriString("/${TEST_BUCKET_NAME}/$key") .queryParam("uploadId", uploadId) .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(uploadRequest), headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(uploadRequest)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART)))) } @Test fun testListMultipartUploads_Ok() { // Arrange - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploads = listOf( MultipartUpload( @@ -683,21 +633,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .queryParam("uploads", "") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - // Assert - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testListMultipartUploads_WithEncodingAndParams_PropagateCorrectly() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val delimiter = "/" @@ -749,20 +695,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testListMultipartUploads_Pagination_ResponseFields() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploads = listOf( @@ -797,15 +740,12 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test @@ -821,20 +761,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test fun testAbortMultipartUpload_NoContent() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -845,14 +782,11 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.DELETE, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + delete(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NO_CONTENT) + .andExpect(status().isNoContent) } @Test @@ -870,23 +804,18 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - // Act - val response = restTemplate.exchange( - uri, - HttpMethod.DELETE, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + delete(uri) + .accept(MediaType.APPLICATION_XML) ) - - // Assert - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test fun testAbortMultipartUpload_NoSuchUpload() { // Arrange - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -901,23 +830,18 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - // Act - val response = restTemplate.exchange( - uri, - HttpMethod.DELETE, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + delete(uri) + .accept(MediaType.APPLICATION_XML) ) - - // Assert - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART)))) } @Test fun testListParts_Ok() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -954,20 +878,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testListParts_WithParams_PropagateCorrectly() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -1010,20 +931,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testListParts_Pagination_ResponseFields() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -1067,15 +985,12 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test @@ -1091,20 +1006,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test fun testListParts_NoSuchUpload() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -1118,20 +1030,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity.EMPTY, - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART)))) } @Test fun testUploadPart_Ok_EtagReturned() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -1141,7 +1050,6 @@ internal class MultipartControllerTest : BaseControllerTest() { multipartService.putPart(eq(TEST_BUCKET_NAME), eq("my/key.txt"), eq(uploadId), eq("1"), eq(temp), any()) ).thenReturn("etag-123") - val headers = HttpHeaders() val uri = UriComponentsBuilder .fromUriString("/${TEST_BUCKET_NAME}/my/key.txt") .queryParam("uploadId", uploadId) @@ -1149,25 +1057,27 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("payload-bytes", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .content("payload-bytes") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.eTag).isEqualTo("\"etag-123\"") + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.ETAG, "\"etag-123\"")) } @Test fun testUploadPartCopy_Ok_VersionIdHeaderWhenVersioned() { - val bucketMeta = bucketMetadata(versioningEnabled = true) + val versioningConfiguration = VersioningConfiguration( + VersioningConfiguration.MFADelete.DISABLED, + VersioningConfiguration.Status.ENABLED, + null + ) + val bucketMeta = bucketMetadata(versioningConfiguration = versioningConfiguration) whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val s3meta = s3ObjectMetadata( key = "source/key.txt", - id = UUID.randomUUID().toString(), versionId = "v1" ) whenever( @@ -1206,16 +1116,14 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity>(headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.getFirst("x-amz-version-id")).isEqualTo("v1") - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(copyResult)) + .andExpect(status().isOk) + .andExpect(header().string("x-amz-version-id", "v1")) + .andExpect(content().string(MAPPER.writeValueAsString(copyResult))) } @Test @@ -1235,20 +1143,18 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test fun testUploadPartCopy_InvalidPartNumber_BadRequest() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) doThrow(S3Exception.INVALID_PART_NUMBER) @@ -1266,20 +1172,18 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART_NUMBER))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART_NUMBER)))) } @Test fun testUploadPartCopy_SourceObjectNotFound() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) doThrow(S3Exception.NO_SUCH_KEY) @@ -1297,20 +1201,18 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_KEY))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_KEY)))) } @Test fun testUploadPartCopy_PreconditionFailed() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val s3meta = s3ObjectMetadata("source/key.txt", UUID.randomUUID().toString()) @@ -1340,25 +1242,22 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.PRECONDITION_FAILED) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.PRECONDITION_FAILED))) + .andExpect(status().isPreconditionFailed) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.PRECONDITION_FAILED)))) } @Test fun testUploadPartCopy_NoVersionHeaderWhenNotVersioned() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val s3meta = s3ObjectMetadata( key = "source/key.txt", - id = UUID.randomUUID().toString(), versionId = "v1" ) whenever(objectService.verifyObjectExists(eq("source-bucket"), eq("source/key.txt"), eq("v1"))) @@ -1382,22 +1281,19 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - // when versioning is disabled, controller should not echo x-amz-version-id - assertThat(response.headers.getFirst("x-amz-version-id")).isNull() - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(copyResult)) + .andExpect(status().isOk) + .andExpect(header().doesNotExist("x-amz-version-id")) + .andExpect(content().string(MAPPER.writeValueAsString(copyResult))) } @Test fun testUploadPartCopy_EncryptionHeadersEchoed() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val s3meta = s3ObjectMetadata("source/key.txt", UUID.randomUUID().toString()) @@ -1433,21 +1329,19 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.getFirst("x-amz-server-side-encryption")).isEqualTo("AES256") - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(copyResult)) + .andExpect(status().isOk) + .andExpect(header().string("x-amz-server-side-encryption", "AES256")) + .andExpect(content().string(MAPPER.writeValueAsString(copyResult))) } @Test fun testUploadPart_WithHeaderChecksum_VerifiedAndReturned() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -1472,17 +1366,15 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("payload-bytes", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) + .content("payload-bytes") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.eTag).isEqualTo("\"etag-321\"") - // checksum header should be echoed - assertThat(response.headers.getFirst("x-amz-checksum-sha256")).isEqualTo(checksum) + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.ETAG, "\"etag-321\"")) + .andExpect(header().string("x-amz-checksum-sha256", checksum)) } @Test @@ -1491,7 +1383,7 @@ internal class MultipartControllerTest : BaseControllerTest() { val temp = java.nio.file.Files.createTempFile("junie", "part") whenever(multipartService.toTempFile(any(), any())).thenReturn(Pair.of(temp, null)) - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -1500,7 +1392,6 @@ internal class MultipartControllerTest : BaseControllerTest() { .whenever(multipartService) .verifyPartNumberLimits("1") - val headers = HttpHeaders() val uri = UriComponentsBuilder .fromUriString("/${TEST_BUCKET_NAME}/my/key.txt") .queryParam("uploadId", uploadId) @@ -1508,17 +1399,13 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - // Act - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("payload-bytes", headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .content("payload-bytes") ) - - // Assert - assertThat(response.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART_NUMBER))) + .andExpect(status().isBadRequest) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.INVALID_PART_NUMBER)))) } @Test @@ -1540,15 +1427,13 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("payload-bytes", HttpHeaders()), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .content("payload-bytes") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test @@ -1556,7 +1441,7 @@ internal class MultipartControllerTest : BaseControllerTest() { val temp = java.nio.file.Files.createTempFile("junie", "part") whenever(multipartService.toTempFile(any(), any())).thenReturn(Pair.of(temp, null)) - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val uploadId = UUID.randomUUID() @@ -1571,20 +1456,18 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity("payload-bytes", HttpHeaders()), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .content("payload-bytes") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_UPLOAD_MULTIPART)))) } @Test fun testCreateMultipartUpload_Ok_ChecksumHeadersPropagated() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val result = InitiateMultipartUploadResult(TEST_BUCKET_NAME, "my/key.txt", "u-1") @@ -1592,7 +1475,7 @@ internal class MultipartControllerTest : BaseControllerTest() { multipartService.createMultipartUpload( eq(TEST_BUCKET_NAME), eq("my/key.txt"), - eq("application/octet-stream"), + startsWith("application/octet-stream"), anyOrNull(), eq(Owner.DEFAULT_OWNER), eq(Owner.DEFAULT_OWNER), @@ -1618,17 +1501,15 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.getFirst("x-amz-checksum-algorithm")).isEqualTo("SHA256") - assertThat(response.headers.getFirst("x-amz-checksum-type")).isEqualTo("FULL_OBJECT") - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(header().string("x-amz-checksum-algorithm", "SHA256")) + .andExpect(header().string("x-amz-checksum-type", "FULL_OBJECT")) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test @@ -1644,22 +1525,17 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - // Act - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity("", HttpHeaders()), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) ) - - // Assert - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET))) + .andExpect(status().isNotFound) + .andExpect(content().string(MAPPER.writeValueAsString(from(S3Exception.NO_SUCH_BUCKET)))) } @Test fun testCreateMultipartUpload_EncryptionHeadersEchoed() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val result = InitiateMultipartUploadResult(TEST_BUCKET_NAME, "enc/key.txt", "u-enc-1") @@ -1690,21 +1566,19 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.getFirst("x-amz-server-side-encryption")).isEqualTo("AES256") - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(header().string("x-amz-server-side-encryption", "AES256")) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testCreateMultipartUpload_StorageClass_Propagated() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val result = InitiateMultipartUploadResult(TEST_BUCKET_NAME, "sc/key.txt", "u-sc-1") @@ -1735,20 +1609,18 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity("", headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } @Test fun testCreateMultipartUpload_NoContentType_PassesNull() { - val bucketMeta = bucketMetadata(versioningEnabled = false) + val bucketMeta = bucketMetadata() whenever(bucketService.verifyBucketExists(TEST_BUCKET_NAME)).thenReturn(bucketMeta) val result = InitiateMultipartUploadResult(TEST_BUCKET_NAME, "noct/key.txt", "u-noct-1") @@ -1777,15 +1649,13 @@ internal class MultipartControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity>(headers), - String::class.java + mockMvc.perform( + post(uri) + .accept(MediaType.APPLICATION_XML) + .headers(headers) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(result)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(result))) } private fun givenBucket() { @@ -1794,73 +1664,8 @@ internal class MultipartControllerTest : BaseControllerTest() { } companion object { - private const val TEST_BUCKET_NAME = "test-bucket" - private val TEST_BUCKET = Bucket( - TEST_BUCKET_NAME, - "us-east-1", - Instant.now().toString(), - Paths.get("/tmp/foo/1") - ) - - private fun createPart(partNumber: Int, size: Long): Part { return Part(partNumber, "someEtag$partNumber", Date(), size) } - - private fun from(e: S3Exception): ErrorResponse { - return ErrorResponse( - e.code, - e.message, - null, - null - ) - } - - private fun bucketMetadata(versioningEnabled: Boolean): BucketMetadata { - val versioning = if (versioningEnabled) VersioningConfiguration(null, VersioningConfiguration.Status.ENABLED, null) else null - return BucketMetadata( - TEST_BUCKET_NAME, - Instant.now().toString(), - versioning, - null, - null, - null, - Paths.get("/tmp/foo/1"), - "us-east-1", - null, - null - ) - } - - private fun s3ObjectMetadata( - key: String, - id: String, - versionId: String? = null - ): S3ObjectMetadata { - return S3ObjectMetadata( - UUID.fromString(id), - key, - "0", - Instant.now().toString(), - "etag", - "application/octet-stream", - System.currentTimeMillis(), - Paths.get("/tmp/foo/1/$key"), - emptyMap(), - emptyList(), - null, - null, - Owner.DEFAULT_OWNER, - emptyMap(), - emptyMap(), - null, - null, - null, - null, - versionId, - false, - ChecksumType.FULL_OBJECT - ) - } } } 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 0133aee0b..775fd77a0 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/ObjectControllerTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/ObjectControllerTest.kt @@ -16,7 +16,6 @@ package com.adobe.testing.s3mock import com.adobe.testing.s3mock.dto.AccessControlPolicy -import com.adobe.testing.s3mock.dto.Bucket import com.adobe.testing.s3mock.dto.CanonicalUser import com.adobe.testing.s3mock.dto.ChecksumAlgorithm import com.adobe.testing.s3mock.dto.ChecksumType @@ -38,15 +37,16 @@ import com.adobe.testing.s3mock.dto.VersioningConfiguration import com.adobe.testing.s3mock.service.BucketService import com.adobe.testing.s3mock.service.MultipartService import com.adobe.testing.s3mock.service.ObjectService -import com.adobe.testing.s3mock.store.BucketMetadata import com.adobe.testing.s3mock.store.KmsKeyStore -import com.adobe.testing.s3mock.store.S3ObjectMetadata import com.adobe.testing.s3mock.util.AwsHttpHeaders import com.adobe.testing.s3mock.util.AwsHttpParameters import com.adobe.testing.s3mock.util.DigestUtil import com.fasterxml.jackson.core.JsonProcessingException import org.apache.commons.lang3.tuple.Pair import org.assertj.core.api.Assertions.assertThat +import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.notNullValue import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyMap @@ -57,28 +57,31 @@ import org.mockito.kotlin.doThrow import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.core.io.ByteArrayResource -import org.springframework.http.HttpEntity +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod -import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import org.springframework.mock.web.MockMultipartFile import org.springframework.test.context.bean.override.mockito.MockitoBean -import org.springframework.util.LinkedMultiValueMap +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.head +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder import software.amazon.awssdk.checksums.DefaultChecksumAlgorithm import java.io.File import java.io.InputStream import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths import java.time.Instant import java.util.UUID @MockitoBean(types = [KmsKeyStore::class, MultipartService::class, BucketController::class, MultipartController::class]) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@WebMvcTest(properties = ["com.adobe.testing.s3mock.region=us-east-1"]) internal class ObjectControllerTest : BaseControllerTest() { @MockitoBean private lateinit var objectService: ObjectService @@ -87,7 +90,7 @@ internal class ObjectControllerTest : BaseControllerTest() { private lateinit var bucketService: BucketService @Autowired - private lateinit var restTemplate: TestRestTemplate + private lateinit var mockMvc: MockMvc @Test @Throws(Exception::class) @@ -131,19 +134,14 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(s3ObjectMetadata(key, digest)) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.TEXT_PLAIN - } - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.PUT, - HttpEntity(testFile.readBytes(), headers), - String::class.java + mockMvc.perform( + put("/test-bucket/$key") + .content(testFile.readBytes()) + .contentType(MediaType.TEXT_PLAIN) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.eTag).isEqualTo("\"$digest\"") + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.ETAG, '"' + digest + '"')) } @@ -189,26 +187,23 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(s3ObjectMetadata(key, digest)) - val optionsResponse = restTemplate.optionsForAllow("/test-bucket/$key") - - assertThat(optionsResponse).contains(HttpMethod.PUT) + mockMvc.perform( + options("/test-bucket/$key") + ) + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.ALLOW, containsString("PUT"))) val origin = "http://www.someurl.com" - val putHeaders = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.TEXT_PLAIN - this.origin = origin - } - val putResponse = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.PUT, - HttpEntity(testFile.readBytes(), putHeaders), - String::class.java + mockMvc.perform( + put("/test-bucket/$key") + .content(testFile.readBytes()) + .contentType(MediaType.TEXT_PLAIN) + .accept(MediaType.APPLICATION_XML) + .header(HttpHeaders.ORIGIN, origin) ) - - assertThat(putResponse.statusCode).isEqualTo(HttpStatus.OK) - assertThat(putResponse.headers.eTag).isEqualTo("\"$digest\"") + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.ETAG, '"' + digest + '"')) } @Test @@ -253,21 +248,16 @@ internal class ObjectControllerTest : BaseControllerTest() { ).thenReturn(s3ObjectMetadata(key, hexDigest)) val base64Digest = DigestUtil.base64Digest(Files.newInputStream(testFile.toPath())) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.TEXT_PLAIN - this[AwsHttpHeaders.CONTENT_MD5] = base64Digest - } - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.PUT, - HttpEntity(testFile.readBytes(), headers), - String::class.java + mockMvc.perform( + put("/test-bucket/$key") + .content(testFile.readBytes()) + .contentType(MediaType.TEXT_PLAIN) + .accept(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.CONTENT_MD5, base64Digest) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.eTag).isEqualTo("\"" + hexDigest + "\"") + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.ETAG, '"' + hexDigest + '"')) } @Test @@ -295,20 +285,15 @@ internal class ObjectControllerTest : BaseControllerTest() { ) val key = "sampleFile.txt" - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.TEXT_PLAIN - this[AwsHttpHeaders.CONTENT_MD5] = base64Digest + 1 - } - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.PUT, - HttpEntity(testFile.readBytes(), headers), - String::class.java + mockMvc.perform( + put("/test-bucket/$key") + .content(testFile.readBytes()) + .contentType(MediaType.TEXT_PLAIN) + .accept(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.CONTENT_MD5, base64Digest + 1) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.BAD_REQUEST) + .andExpect(status().isBadRequest) } @Test @@ -325,21 +310,14 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists(TEST_BUCKET_NAME, key, null)) .thenReturn(expectedS3ObjectMetadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.ALL) - this.contentType = MediaType.TEXT_PLAIN - } - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.GET, - HttpEntity(headers), - ByteArray::class.java + mockMvc.perform( + get("/test-bucket/$key") + .accept(MediaType.ALL) + .contentType(MediaType.TEXT_PLAIN) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION]).containsExactly(encryption) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID]) - .containsExactly(encryptionKey) + .andExpect(status().isOk) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION, encryption)) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, encryptionKey)) } @Test @@ -355,21 +333,14 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists("test-bucket", key, null)) .thenReturn(expectedS3ObjectMetadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.TEXT_PLAIN - } - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.HEAD, - HttpEntity(headers), - Void::class.java + mockMvc.perform( + head("/test-bucket/$key") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.TEXT_PLAIN) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION]).containsExactly(encryption) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID]) - .containsExactly(encryptionKey) + .andExpect(status().isOk) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION, encryption)) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, encryptionKey)) } @Test @@ -379,17 +350,12 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists("test-bucket", key, null)) .thenThrow(S3Exception.NO_SUCH_KEY) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.TEXT_PLAIN - } - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.HEAD, - HttpEntity(headers), - String::class.java + mockMvc.perform( + head("/test-bucket/$key") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.TEXT_PLAIN) ) - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) + .andExpect(status().isNotFound) } @Test @@ -410,24 +376,18 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.getAcl("test-bucket", key, null)).thenReturn(policy) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.ACL, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(policy)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(policy))) } @Test @@ -446,23 +406,18 @@ internal class ObjectControllerTest : BaseControllerTest() { listOf(Grant(grantee, Grant.Permission.FULL_CONTROL)) ) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.ACL, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity(MAPPER.writeValueAsString(policy), headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(policy)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + .andExpect(status().isOk) verify(objectService).setAcl("test-bucket", key, null, policy) } @@ -486,23 +441,18 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists("test-bucket", key, null)) .thenReturn(s3ObjectMetadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.TAGGING, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java - ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(tagging)) + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + ) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(tagging))) } @Test @@ -521,24 +471,20 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.TAGGING, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity(MAPPER.writeValueAsString(tagging), headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(tagging)) ) + .andExpect(status().isOk) verify(objectService).setObjectTags("test-bucket", key, null, tagging.tagSet.tags) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) } @Test @@ -556,23 +502,18 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectLockConfiguration("test-bucket", key, null)) .thenReturn(s3ObjectMetadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.RETENTION, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java - ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(retention)) + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + ) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(retention))) } @Test @@ -583,24 +524,20 @@ internal class ObjectControllerTest : BaseControllerTest() { val instant = Instant.ofEpochMilli(1514477008120L) val retention = Retention(Mode.COMPLIANCE, instant) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.RETENTION, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity(MAPPER.writeValueAsString(retention), headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(retention)) ) + .andExpect(status().isOk) verify(objectService).setRetention("test-bucket", key, null, retention) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) } @Test @@ -614,24 +551,18 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists("test-bucket", key, null)) .thenReturn(s3ObjectMetadata(key, digest)) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.ALL) - this.set("Range", "bytes=1-2") - } + val total = testFile.length() - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.GET, - HttpEntity(headers), - ByteArray::class.java + mockMvc.perform( + get("/test-bucket/$key") + .accept(MediaType.ALL) + .header("Range", "bytes=1-2") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.PARTIAL_CONTENT) - val total = testFile.length() - assertThat(response.headers.getFirst(HttpHeaders.CONTENT_RANGE)).isEqualTo("bytes 1-2/$total") - assertThat(response.headers.getFirst(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes") - assertThat(response.headers.contentLength).isEqualTo(2) - assertThat(response.headers.eTag).isEqualTo("\"$digest\"") + .andExpect(status().isPartialContent) + .andExpect(header().string(HttpHeaders.CONTENT_RANGE, "bytes 1-2/$total")) + .andExpect(header().string(HttpHeaders.ACCEPT_RANGES, "bytes")) + .andExpect(header().longValue(HttpHeaders.CONTENT_LENGTH, 2)) + .andExpect(header().string(HttpHeaders.ETAG, '"' + digest + '"')) } @Test @@ -641,23 +572,17 @@ internal class ObjectControllerTest : BaseControllerTest() { val s3ObjectMetadata = s3ObjectMetadata(key, UUID.randomUUID().toString()) whenever(objectService.verifyObjectExists("test-bucket", key, null)).thenReturn(s3ObjectMetadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.TAGGING, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.DELETE, - HttpEntity(headers), - String::class.java + mockMvc.perform( + org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete(uri) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NO_CONTENT) + .andExpect(status().isNoContent) verify(objectService).setObjectTags("test-bucket", key, null, null) } @@ -674,25 +599,19 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists("test-bucket", key, null)).thenReturn(metadata) whenever(objectService.verifyObjectLockConfiguration("test-bucket", key, null)).thenReturn(metadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.LEGAL_HOLD, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(legalHold)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(legalHold))) } @Test @@ -701,24 +620,19 @@ internal class ObjectControllerTest : BaseControllerTest() { val key = "locked" val legalHold = LegalHold(LegalHold.Status.OFF) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.LEGAL_HOLD, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.PUT, - HttpEntity(MAPPER.writeValueAsString(legalHold), headers), - String::class.java + mockMvc.perform( + put(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(legalHold)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) + .andExpect(status().isOk) verify(objectService).setLegalHold("test-bucket", key, null, legalHold) } @@ -731,24 +645,12 @@ internal class ObjectControllerTest : BaseControllerTest() { val metadata = s3ObjectMetadata(key, hex) whenever(objectService.verifyObjectExists("test-bucket", key, null)).thenReturn(metadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - this.add(AwsHttpHeaders.X_AMZ_OBJECT_ATTRIBUTES, "ETag,Checksum,ObjectSize,StorageClass") - } val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.ATTRIBUTES, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java - ) - val expected = GetObjectAttributesOutput( null, hex, @@ -756,8 +658,15 @@ internal class ObjectControllerTest : BaseControllerTest() { testFile.length(), StorageClass.STANDARD ) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.X_AMZ_OBJECT_ATTRIBUTES, "ETag,Checksum,ObjectSize,StorageClass") + ) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -779,25 +688,20 @@ internal class ObjectControllerTest : BaseControllerTest() { ) whenever(objectService.deleteObjects("test-bucket", body)).thenReturn(expected) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - } val uri = UriComponentsBuilder .fromUriString("/test-bucket") .queryParam(AwsHttpParameters.DELETE, "ignored") .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.POST, - HttpEntity(MAPPER.writeValueAsString(body), headers), - String::class.java + mockMvc.perform( + org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post(uri) + .contentType(MediaType.APPLICATION_XML) + .accept(MediaType.APPLICATION_XML) + .content(MAPPER.writeValueAsString(body)) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.body).isEqualTo(MAPPER.writeValueAsString(expected)) + .andExpect(status().isOk) + .andExpect(content().string(MAPPER.writeValueAsString(expected))) } @Test @@ -841,26 +745,16 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(copiedMeta) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - // indicate REPLACE to test store/user headers path too (no specific headers asserted here) - this[AwsHttpHeaders.X_AMZ_METADATA_DIRECTIVE] = "REPLACE" - this[AwsHttpHeaders.X_AMZ_COPY_SOURCE] = "/$sourceBucket/$sourceKey?versionId=$sourceVersion" - } - - val response = restTemplate.exchange( - "/$targetBucket/$targetKey", - HttpMethod.PUT, - HttpEntity(null, headers), - String::class.java + mockMvc.perform( + put("/$targetBucket/$targetKey") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.X_AMZ_METADATA_DIRECTIVE, "REPLACE") + .header(AwsHttpHeaders.X_AMZ_COPY_SOURCE, "/$sourceBucket/$sourceKey?versionId=$sourceVersion") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - // Source version header must be present - assertThat(response.headers[AwsHttpHeaders.X_AMZ_COPY_SOURCE_VERSION_ID]).containsExactly(sourceVersion) - // Target version header must be present (copy target version) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_VERSION_ID]).containsExactly("tv1") + .andExpect(status().isOk) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_COPY_SOURCE_VERSION_ID, sourceVersion)) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_VERSION_ID, "tv1")) } @Test @@ -886,22 +780,15 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(null) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - this[AwsHttpHeaders.X_AMZ_COPY_SOURCE] = "/$sourceBucket/$sourceKey" - } - - val response = restTemplate.exchange( - "/$targetBucket/$targetKey", - HttpMethod.PUT, - HttpEntity(null, headers), - String::class.java + mockMvc.perform( + put("/$targetBucket/$targetKey") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.X_AMZ_COPY_SOURCE, "/$sourceBucket/$sourceKey") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NOT_FOUND) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION]).containsExactly("aws:kms") - assertThat(response.headers[AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID]).containsExactly("kms-key") + .andExpect(status().isNotFound) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION, "aws:kms")) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID, "kms-key")) } @Test @@ -933,18 +820,12 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.deleteObject(bucket, key, null)).thenReturn(true) - val response = restTemplate.exchange( - "/$bucket/$key", - HttpMethod.DELETE, - HttpEntity(HttpHeaders()), - String::class.java + mockMvc.perform( + org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete("/$bucket/$key") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NO_CONTENT) - // Controller sets delete marker based on follow-up verify throwing NO_SUCH_KEY_DELETE_MARKER - assertThat(response.headers[AwsHttpHeaders.X_AMZ_DELETE_MARKER]).containsExactly("true") - // When versioning enabled and original metadata had versionId, it should be echoed - assertThat(response.headers[AwsHttpHeaders.X_AMZ_VERSION_ID]).containsExactly("v1") + .andExpect(status().isNoContent) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_DELETE_MARKER, "true")) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_VERSION_ID, "v1")) } @Test @@ -967,24 +848,15 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(returned) - // Build multipart request - val fileResource = object : ByteArrayResource(testFile.readBytes()) { - override fun getFilename(): String = key - } - val parts = LinkedMultiValueMap() - parts.add("key", key) - parts.add("file", HttpEntity(fileResource)) - - val headers = HttpHeaders().apply { contentType = MediaType.MULTIPART_FORM_DATA } - val response = restTemplate.postForEntity( - "/$bucket", - HttpEntity(parts, headers), - String::class.java + mockMvc.perform( + multipart("/$bucket") + .file(MockMultipartFile("file", key, MediaType.APPLICATION_OCTET_STREAM_VALUE, testFile.readBytes())) + .param("key", key) + .accept(MediaType.APPLICATION_XML) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.eTag).isEqualTo(returned.etag) + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.ETAG, notNullValue(String::class.java))) } @Test @@ -999,21 +871,13 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists("test-bucket", key, null)).thenReturn(meta) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.ALL) - this[AwsHttpHeaders.X_AMZ_CHECKSUM_MODE] = "ENABLED" - } - - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.GET, - HttpEntity(headers), - ByteArray::class.java + mockMvc.perform( + get("/test-bucket/$key") + .accept(MediaType.ALL) + .header(AwsHttpHeaders.X_AMZ_CHECKSUM_MODE, "ENABLED") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_CHECKSUM_CRC32]) - .containsExactly("abcd1234") + .andExpect(status().isOk) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_CHECKSUM_CRC32, "abcd1234")) } @Test @@ -1032,16 +896,12 @@ internal class ObjectControllerTest : BaseControllerTest() { .build() .toString() - val response = restTemplate.exchange( - uri, - HttpMethod.HEAD, - HttpEntity(HttpHeaders()), - Void::class.java + mockMvc.perform( + head(uri) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.contentType?.toString()).isEqualTo(contentType) - assertThat(response.headers.getFirst(HttpHeaders.CONTENT_DISPOSITION)).isEqualTo(contentDisposition) + .andExpect(status().isOk) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, contentType)) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)) } @Test @@ -1051,15 +911,11 @@ internal class ObjectControllerTest : BaseControllerTest() { val meta = s3ObjectMetadata(key) whenever(objectService.verifyObjectExists("test-bucket", key, null)).thenReturn(meta) - val headers = HttpHeaders().apply { this.set("Range", "bytes=9999999-10000000") } - val response = restTemplate.exchange( - "/test-bucket/$key", - HttpMethod.GET, - HttpEntity(headers), - String::class.java + mockMvc.perform( + get("/test-bucket/$key") + .header("Range", "bytes=9999999-10000000") ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE) + .andExpect(status().isRequestedRangeNotSatisfiable) } @Test @@ -1082,24 +938,19 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(returned) - val fileResource = object : ByteArrayResource(testFile.readBytes()) { override fun getFilename(): String = key } - val parts = LinkedMultiValueMap().apply { - add("key", key) - add("file", HttpEntity(fileResource)) - add("tagging", MAPPER.writeValueAsString(tagging)) - add("x-amz-storage-class", StorageClass.STANDARD.name) - } - - val headers = HttpHeaders().apply { contentType = MediaType.MULTIPART_FORM_DATA } - - val response = restTemplate.postForEntity( - "/$bucket", - HttpEntity(parts, headers), - String::class.java - ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) - assertThat(response.headers.eTag).isEqualTo(returned.etag) + mockMvc.perform( + multipart("/$bucket") + .file(MockMultipartFile("file", key, MediaType.APPLICATION_OCTET_STREAM_VALUE, testFile.readBytes())) + .param("key", key) + .param("tagging", MAPPER.writeValueAsString(tagging)) + .param("x-amz-storage-class", StorageClass.STANDARD.name) + .accept(MediaType.APPLICATION_XML) + ) + .andExpect(status().isOk) + .andExpect( + header().string(HttpHeaders.ETAG, + notNullValue(String::class.java) + )) // verify storage class and tags were passed verify(objectService).putS3Object( eq(bucket), eq(key), any(), anyMap(), any(Path::class.java), anyMap(), anyMap(), eq(tagging.tagSet.tags), isNull(), isNull(), eq(Owner.DEFAULT_OWNER), eq(StorageClass.STANDARD) @@ -1131,24 +982,16 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(s3ObjectMetadata) - val headers = HttpHeaders().apply { - this[HttpHeaders.IF_MATCH] = listOf("\"etag-123\"") - this[AwsHttpHeaders.X_AMZ_SDK_CHECKSUM_ALGORITHM] = "CRC32" - contentType = MediaType.APPLICATION_OCTET_STREAM - } - - val resp = restTemplate.exchange( - "/$bucket/$key", - HttpMethod.PUT, - HttpEntity(src.readBytes(), headers), - String::class.java + mockMvc.perform( + put("/$bucket/$key") + .content(src.readBytes()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.IF_MATCH, '"' + "etag-123" + '"') + .header(AwsHttpHeaders.X_AMZ_SDK_CHECKSUM_ALGORITHM, "CRC32") ) - - assertThat(resp.statusCode).isEqualTo(HttpStatus.OK) - // checksum header echoed from metadata - assertThat(resp.headers[AwsHttpHeaders.X_AMZ_CHECKSUM_CRC32]).containsExactly("crc32Value") - // object size header present - assertThat(resp.headers[AwsHttpHeaders.X_AMZ_OBJECT_SIZE]).containsExactly(s3ObjectMetadata.size()) + .andExpect(status().isOk) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_CHECKSUM_CRC32, "crc32Value")) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_OBJECT_SIZE, s3ObjectMetadata.size())) // verify matching path used and checksum verification invoked verify(objectService).verifyObjectMatching(eq(bucket), eq(key), any(), isNull()) verify(objectService).verifyChecksum(eq(temp), eq("crc32Value"), eq(ChecksumAlgorithm.CRC32)) @@ -1166,26 +1009,21 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists("test-bucket", key, null)).thenReturn(s3ObjectMetadata) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this[AwsHttpHeaders.X_AMZ_OBJECT_ATTRIBUTES] = "Checksum,ObjectSize" - } - val uri = UriComponentsBuilder .fromUriString("/test-bucket/$key") .queryParam(AwsHttpParameters.ATTRIBUTES, "ignored") .build() .toString() - val resp = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java + val mvcResult = mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.X_AMZ_OBJECT_ATTRIBUTES, "Checksum,ObjectSize") ) + .andExpect(status().isOk) + .andReturn() - assertThat(resp.statusCode).isEqualTo(HttpStatus.OK) - val got = MAPPER.readValue(resp.body, GetObjectAttributesOutput::class.java) + val got = MAPPER.readValue(mvcResult.response.contentAsString, GetObjectAttributesOutput::class.java) // only selected fields should be present assertThat(got.etag()).isNull() assertThat(got.storageClass()).isNull() @@ -1218,25 +1056,19 @@ internal class ObjectControllerTest : BaseControllerTest() { ) ).thenReturn(copied) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this.contentType = MediaType.APPLICATION_XML - this[AwsHttpHeaders.X_AMZ_COPY_SOURCE] = "/$sourceBucket/$sourceKey" - this[AwsHttpHeaders.X_AMZ_METADATA_DIRECTIVE] = "COPY" - this[AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_MATCH] = "\"etag-1\"" - this[AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_NONE_MATCH] = "\"etag-2\"" - this[AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_MODIFIED_SINCE] = Instant.now().toString() - this[AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_UNMODIFIED_SINCE] = Instant.now().minusSeconds(60).toString() - } - - val response = restTemplate.exchange( - "/$targetBucket/$targetKey", - HttpMethod.PUT, - HttpEntity(null, headers), - String::class.java + mockMvc.perform( + put("/$targetBucket/$targetKey") + .accept(MediaType.APPLICATION_XML) + .contentType(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.X_AMZ_COPY_SOURCE, "/$sourceBucket/$sourceKey") + .header(AwsHttpHeaders.X_AMZ_METADATA_DIRECTIVE, "COPY") + .header(AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_MATCH, "\"etag-1\"") + .header(AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_NONE_MATCH, "\"etag-2\"") + .header(AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_MODIFIED_SINCE, Instant.now().toString()) + .header(AwsHttpHeaders.X_AMZ_COPY_SOURCE_IF_UNMODIFIED_SINCE, Instant.now().minusSeconds(60).toString()) ) + .andExpect(status().isOk) - assertThat(response.statusCode).isEqualTo(HttpStatus.OK) // verify conditional headers reached the service verifier verify(objectService).verifyObjectMatchingForCopy( eq(listOf("\"etag-1\"")), @@ -1266,20 +1098,14 @@ internal class ObjectControllerTest : BaseControllerTest() { val lm = Instant.now() val size = 123L - val headers = HttpHeaders().apply { - this[AwsHttpHeaders.X_AMZ_IF_MATCH_LAST_MODIFIED_TIME] = lm.toString() - this[AwsHttpHeaders.X_AMZ_IF_MATCH_SIZE] = size.toString() - } - val response = restTemplate.exchange( - "/$bucket/$key", - HttpMethod.DELETE, - HttpEntity(headers), - String::class.java + mockMvc.perform( + org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete("/$bucket/$key") + .header(AwsHttpHeaders.X_AMZ_IF_MATCH_LAST_MODIFIED_TIME, lm.toString()) + .header(AwsHttpHeaders.X_AMZ_IF_MATCH_SIZE, size.toString()) ) - - assertThat(response.statusCode).isEqualTo(HttpStatus.NO_CONTENT) - assertThat(response.headers[AwsHttpHeaders.X_AMZ_DELETE_MARKER]).containsExactly("false") + .andExpect(status().isNoContent) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_DELETE_MARKER, "false")) // verify match headers forwarded with null metadata verify(objectService).verifyObjectMatching( @@ -1309,15 +1135,12 @@ internal class ObjectControllerTest : BaseControllerTest() { val meta = s3ObjectMetadata(key, versionId = "v-123") whenever(objectService.verifyObjectExists(bucket, key, null)).thenReturn(meta) - val resp = restTemplate.exchange( - "/$bucket/$key", - HttpMethod.HEAD, - HttpEntity(HttpHeaders()), - Void::class.java + mockMvc.perform( + head("/$bucket/$key") ) - - assertThat(resp.statusCode).isEqualTo(HttpStatus.OK) - assertThat(resp.headers[AwsHttpHeaders.X_AMZ_VERSION_ID]).containsExactly("v-123") + .andExpect(status().isOk) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_VERSION_ID, "v-123")) + .andReturn() } @Test @@ -1338,15 +1161,11 @@ internal class ObjectControllerTest : BaseControllerTest() { val meta = s3ObjectMetadata(key, versionId = "v-9") whenever(objectService.verifyObjectExists(bucket, key, null)).thenReturn(meta) - val resp = restTemplate.exchange( - "/$bucket/$key", - HttpMethod.GET, - HttpEntity(HttpHeaders()), - ByteArray::class.java + mockMvc.perform( + get("/$bucket/$key") ) - - assertThat(resp.statusCode).isEqualTo(HttpStatus.OK) - assertThat(resp.headers[AwsHttpHeaders.X_AMZ_VERSION_ID]).containsExactly("v-9") + .andExpect(status().isOk) + .andExpect(header().string(AwsHttpHeaders.X_AMZ_VERSION_ID, "v-9")) } @Test @@ -1373,20 +1192,16 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(objectService.verifyObjectExists(bucket, key, null)).thenReturn(s3ObjectMetadata) - val resp = restTemplate.exchange( - "/$bucket/$key", - HttpMethod.GET, - HttpEntity(HttpHeaders()), - ByteArray::class.java + mockMvc.perform( + get("/$bucket/$key") ) - - assertThat(resp.statusCode).isEqualTo(HttpStatus.OK) - // store headers propagated - assertThat(resp.headers.getFirst(HttpHeaders.CACHE_CONTROL)).isEqualTo("max-age=3600") - assertThat(resp.headers.getFirst(HttpHeaders.CONTENT_LANGUAGE)).isEqualTo("en") - // user metadata transformed to x-amz-meta-* - assertThat(resp.headers.getFirst("x-amz-meta-foo")).isEqualTo("bar") - assertThat(resp.headers.getFirst("x-amz-meta-answer")).isEqualTo("42") + .andExpect(status().isOk) + // store headers propagated + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, "max-age=3600")) + .andExpect(header().string(HttpHeaders.CONTENT_LANGUAGE, "en")) + // user metadata transformed to x-amz-meta-* + .andExpect(header().string("x-amz-meta-foo", "bar")) + .andExpect(header().string("x-amz-meta-answer", "42")) } @Test @@ -1411,31 +1226,27 @@ internal class ObjectControllerTest : BaseControllerTest() { val meta = s3ObjectMetadata(key, hex, versionId = "va1") whenever(objectService.verifyObjectExists(bucket, key, null)).thenReturn(meta) - val headers = HttpHeaders().apply { - this.accept = listOf(MediaType.APPLICATION_XML) - this[AwsHttpHeaders.X_AMZ_OBJECT_ATTRIBUTES] = "ETag" - } val uri = UriComponentsBuilder .fromUriString("/$bucket/$key") .queryParam(AwsHttpParameters.ATTRIBUTES, "ignored") .build() .toString() - val resp = restTemplate.exchange( - uri, - HttpMethod.GET, - HttpEntity(headers), - String::class.java - ) - - assertThat(resp.statusCode).isEqualTo(HttpStatus.OK) - // version header present - assertThat(resp.headers[AwsHttpHeaders.X_AMZ_VERSION_ID]).containsExactly("va1") - // ETag must be without quotes in XML body - assertThat(resp.body).contains("$hex") - // other fields not requested should not appear - assertThat(resp.body).doesNotContain("") - assertThat(resp.body).doesNotContain("") + mockMvc.perform( + get(uri) + .accept(MediaType.APPLICATION_XML) + .header(AwsHttpHeaders.X_AMZ_OBJECT_ATTRIBUTES, "ETag") + ) + .andExpect(status().isOk) + // version header present + .andExpect(header().string(AwsHttpHeaders.X_AMZ_VERSION_ID, "va1")) + .andExpect { + // ETag must be without quotes in XML body + content().string(containsString("\"$hex\"")) + // other fields not requested should not appear + content().string(not(containsString(""))) + content().string(not(containsString(""))) + } } private fun givenBucket() { @@ -1443,99 +1254,6 @@ internal class ObjectControllerTest : BaseControllerTest() { whenever(bucketService.doesBucketExist(TEST_BUCKET_NAME)).thenReturn(true) whenever(bucketService.verifyBucketExists("test-bucket")).thenReturn(TEST_BUCKETMETADATA) } - - companion object { - private const val TEST_BUCKET_NAME = "test-bucket" - private val TEST_BUCKET = Bucket(TEST_BUCKET_NAME, "us-east-1", Instant.now().toString(), Paths.get("/tmp/foo/1")) - private val TEST_BUCKETMETADATA = bucketMetadata() - private const val UPLOAD_FILE_NAME = "src/test/resources/sampleFile.txt" - - fun s3ObjectEncrypted( - id: String, - digest: String = UUID.randomUUID().toString(), - encryption: String?, - encryptionKey: String? - ): S3ObjectMetadata { - return s3ObjectMetadata( - id, digest, encryption, encryptionKey, - ) - } - - fun bucketMetadata( - name: String = TEST_BUCKET_NAME, - creationDate: String = Instant.now().toString(), - path: Path = Paths.get("/tmp/foo/1"), - bucketRegion: String = "us-east-1", - versioningConfiguration: VersioningConfiguration? = null - ): BucketMetadata { - return BucketMetadata( - name, - creationDate, - versioningConfiguration, - null, - null, - null, - path, - bucketRegion, - null, - null, - ) - } - - @JvmOverloads - fun s3ObjectMetadata( - id: String, - digest: String = UUID.randomUUID().toString(), - encryption: String? = null, - encryptionKey: String? = null, - retention: Retention? = null, - tags: List? = null, - legalHold: LegalHold? = null, - versionId: String? = null, - checksum: String? = null, - checksumType: ChecksumType? = ChecksumType.FULL_OBJECT, - checksumAlgorithm: ChecksumAlgorithm? = null, - userMetadata: Map? = null, - storeHeaders: Map? = null, - ): S3ObjectMetadata { - return S3ObjectMetadata( - UUID.randomUUID(), - id, - Path.of(UPLOAD_FILE_NAME).toFile().length().toString(), - "1234", - digest, - "text/plain", - 1L, - Path.of(UPLOAD_FILE_NAME), - userMetadata, - tags, - legalHold, - retention, - Owner.DEFAULT_OWNER, - storeHeaders, - encryptionHeaders(encryption, encryptionKey), - checksumAlgorithm, - checksum, - null, - null, - versionId, - false, - checksumType - ) - } - - private fun encryptionHeaders(encryption: String?, encryptionKey: String?): Map { - val pairs = mutableListOf>() - if (encryption != null) { - pairs.add(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION to encryption) - } - if(encryptionKey!= null) { - pairs.add(AwsHttpHeaders.X_AMZ_SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID to encryptionKey) - } - - return pairs.associate { it.first to it.second } - } - } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/TaggingHeaderConverterTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/TaggingHeaderConverterTest.kt index 23ed26017..034cba411 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/TaggingHeaderConverterTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/TaggingHeaderConverterTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 Adobe. + * Copyright 2017-2025 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,16 +33,13 @@ internal class TaggingHeaderConverterTest { val singleTag = tag(1) val actual = iut.convert(singleTag) assertThat(actual).isNotEmpty().hasSize(1) - assertThat(actual!![0]).isEqualTo(Tag(singleTag)) + assertThat(requireNotNull(actual)[0]).isEqualTo(Tag(singleTag)) } @Test fun testMultipleTagsConversion() { val iut = TaggingHeaderConverter() - val tags = mutableListOf() - for (i in 0..4) { - tags.add(tag(i)) - } + val tags = (0..4).map { tag(it) } val actual = iut.convert(tags.joinToString(separator = "&")) assertThat(actual) .isNotEmpty() @@ -56,7 +53,5 @@ internal class TaggingHeaderConverterTest { ) } - private fun tag(i: Int): String { - return "tag$i=value$i" - } + private fun tag(i: Int): String = "tag$i=value$i" } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt index 39505a617..5c8ce268c 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/AccessControlPolicyTest.kt @@ -29,15 +29,15 @@ internal class AccessControlPolicyTest { AccessControlPolicy::class.java, testInfo ) - val owner = iut.getOwner() + val owner = iut.owner assertThat(owner).isNotNull() assertThat(owner.id).isEqualTo( "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a" ) assertThat(owner.displayName).isEqualTo("mtd@amazon.com") - assertThat(iut.getAccessControlList()).hasSize(3) + assertThat(iut.accessControlList).hasSize(3) - iut.getAccessControlList()[0].also { + iut.accessControlList[0].also { assertThat(it.permission).isEqualTo(Grant.Permission.FULL_CONTROL) assertThat(it.grantee).isNotNull() assertThat(it.grantee).isInstanceOf(CanonicalUser::class.java) @@ -46,14 +46,14 @@ internal class AccessControlPolicyTest { assertThat(user.displayName()).isEqualTo("mtd@amazon.com") } - iut.getAccessControlList()[1].also { + iut.accessControlList[1].also { assertThat(it.permission).isEqualTo(Grant.Permission.WRITE) assertThat(it.grantee).isNotNull() assertThat(it.grantee).isInstanceOf(Group::class.java) val group = it.grantee as Group assertThat(group.uri()).isEqualTo(URI.create("http://acs.amazonaws.com/groups/s3/LogDelivery")) } - iut.getAccessControlList()[2].also { + iut.accessControlList[2].also { assertThat(it.permission).isEqualTo(Grant.Permission.WRITE_ACP) assertThat(it.grantee).isNotNull() assertThat(it.grantee).isInstanceOf(AmazonCustomerByEmail::class.java) diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/DeleteResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/DeleteResultTest.kt index e28db8d03..08b7b9509 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/DeleteResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/DeleteResultTest.kt @@ -26,14 +26,13 @@ internal class DeleteResultTest { fun testSerialization(testInfo: TestInfo) { val iut = DeleteResult(ArrayList(), ArrayList()) assertThat(iut).isNotNull() - val count = 2 - for (i in 0 until count) { + repeat(2) { val deletedObject = S3ObjectIdentifier( - "key$i", - "etag$i", - "lastModifiedTime$i", - "size$i", - "versionId$i" + "key$it", + "etag$it", + "lastModifiedTime$it", + "size$it", + "versionId$it" ) iut.addDeletedObject(DeletedS3Object.from(deletedObject)) } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest.kt index cc2884afc..84065fc7e 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/GetObjectAttributesOutputTest.kt @@ -67,14 +67,12 @@ internal class GetObjectAttributesOutputTest { serializeAndAssert(iut, testInfo) } - fun checksum(): Checksum { - return Checksum( - "checksumCRC32", - "checksumCRC32C", - "checksumCRC64NVME", - "checksumSHA1", - "checksumSHA256", - ChecksumType.FULL_OBJECT, - ) - } + fun checksum(): Checksum = Checksum( + "checksumCRC32", + "checksumCRC32C", + "checksumCRC64NVME", + "checksumSHA1", + "checksumSHA256", + ChecksumType.FULL_OBJECT, + ) } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt index 3451c1439..396e770a6 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListAllMyBucketsResultTest.kt @@ -38,16 +38,14 @@ internal class ListAllMyBucketsResultTest { } private fun createBuckets(count: Int = 2): Buckets { - val buckets = ArrayList() - for (i in 0 until count) { - val bucket = Bucket( - "us-east-1", - "creationDate", - "name-$i", - Paths.get("/tmp/foo") + val buckets = (0 until count).map { + Bucket( + "us-east-1", + "creationDate", + "name-$it", + Paths.get("/tmp/foo") ) - buckets.add(bucket) } - return Buckets(buckets) + return Buckets(ArrayList(buckets)) } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt index 905211d69..451b0e431 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultTest.kt @@ -43,22 +43,18 @@ internal class ListBucketResultTest { } private fun createBucketContents(count: Int = 2): List { - val s3ObjectList = ArrayList() - for (i in 0 until count) { + return (0 until count).map { S3Object( ChecksumAlgorithm.SHA256, ChecksumType.FULL_OBJECT, "\"fba9dede5f27731c9771645a39863328\"", - "key$i", + "key$it", "2009-10-12T17:50:30.000Z", - Owner("displayName", (10L + i).toString()), + Owner("displayName", (10L + it).toString()), null, "434234", StorageClass.STANDARD - ).also { - s3ObjectList.add(it) - } + ) } - return s3ObjectList } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt index afd34a1fe..24f66c160 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListBucketResultV2Test.kt @@ -48,22 +48,18 @@ internal class ListBucketResultV2Test { } private fun createBucketContents(count: Int): List { - val s3ObjectList = ArrayList() - for (i in 0 until count) { + return (0 until count).map { S3Object( ChecksumAlgorithm.SHA256, ChecksumType.FULL_OBJECT, "\"fba9dede5f27731c9771645a39863328\"", - "key$i", + "key$it", "2009-10-12T17:50:30.000Z", - Owner("displayName", (10L + i).toString()), + Owner("displayName", (10L + it).toString()), null, "434234", StorageClass.STANDARD - ).also { - s3ObjectList.add(it) - } + ) } - return s3ObjectList } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt index dfc761b1d..063dcd85f 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListMultipartUploadsResultTest.kt @@ -46,21 +46,17 @@ internal class ListMultipartUploadsResultTest { } private fun createMultipartUploads(count: Int): List { - val multipartUploads = ArrayList() - for (i in 0 until count) { - val multipartUpload = - MultipartUpload( - ChecksumAlgorithm.SHA256, - ChecksumType.COMPOSITE, - Date(1514477008120L), - Owner("displayName100$i", (100L + i).toString()), - "key$i", - Owner("displayName10$i", (10L + i).toString()), - StorageClass.STANDARD, - "uploadId$i", - ) - multipartUploads.add(multipartUpload) + return (0 until count).map { + MultipartUpload( + ChecksumAlgorithm.SHA256, + ChecksumType.COMPOSITE, + Date(1514477008120L), + Owner("displayName100$it", (100L + it).toString()), + "key$it", + Owner("displayName10$it", (10L + it).toString()), + StorageClass.STANDARD, + "uploadId$it", + ) } - return multipartUploads } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt index ae093f567..872e914e4 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/ListPartsResultTest.kt @@ -48,10 +48,6 @@ internal class ListPartsResultTest { } private fun createParts(count: Int = 2): List { - val parts = ArrayList() - for (i in 1..count) { - parts.add(Part(i, "etag$i", Date(1514477008120L), 10L + i)) - } - return parts + return (1..count).map { Part(it, "etag$it", Date(1514477008120L), 10L + it) } } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/TaggingTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/TaggingTest.kt index fda8cc994..acd82b5be 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/dto/TaggingTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/dto/TaggingTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 Adobe. + * Copyright 2017-2025 Adobe. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,8 +49,6 @@ internal class TaggingTest { } companion object { - private fun createTag(counter: Int): Tag { - return Tag("key$counter", "val$counter") - } + private fun createTag(counter: Int): Tag = Tag("key$counter", "val$counter") } } diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/service/BucketServiceTest.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/service/BucketServiceTest.kt index 910c9163d..088ee917e 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/service/BucketServiceTest.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/service/BucketServiceTest.kt @@ -76,7 +76,7 @@ internal class BucketServiceTest : ServiceTestBase() { private fun assertPrefix(key: String, prefix: String?) { val id = UUID.randomUUID() - whenever(bucketStore.lookupKeysInBucket(prefix, TEST_BUCKET_NAME)).thenReturn(listOf(id)) + whenever(bucketStore.lookupIdsInBucket(prefix, TEST_BUCKET_NAME)).thenReturn(listOf(id)) val bucketMetadata = metadataFrom(TEST_BUCKET_NAME) whenever(bucketStore.getBucketMetadata(TEST_BUCKET_NAME)).thenReturn(bucketMetadata) whenever(objectStore.getS3ObjectMetadata(bucketMetadata, id, null)).thenReturn(s3ObjectMetadata(id, key)) 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 3350d55e3..95a67f1cd 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 @@ -131,7 +131,7 @@ internal abstract class ServiceTestBase { whenever(objectStore.getS3ObjectMetadata(bucketMetadata, id, null)) .thenReturn(s3ObjectMetadata(id, s3Object.key)) } - whenever(bucketStore.lookupKeysInBucket(prefix, name)).thenReturn(ids) + whenever(bucketStore.lookupIdsInBucket(prefix, name)).thenReturn(ids) return s3Objects } 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 cbaaf7078..092910ec7 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 @@ -16,14 +16,23 @@ package com.adobe.testing.s3mock.store +import com.adobe.testing.s3mock.dto.BucketInfo import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration +import com.adobe.testing.s3mock.dto.BucketType +import com.adobe.testing.s3mock.dto.DataRedundancy import com.adobe.testing.s3mock.dto.LifecycleRule import com.adobe.testing.s3mock.dto.LifecycleRuleFilter +import com.adobe.testing.s3mock.dto.LocationInfo +import com.adobe.testing.s3mock.dto.LocationType +import com.adobe.testing.s3mock.dto.ObjectLockConfiguration import com.adobe.testing.s3mock.dto.ObjectLockEnabled +import com.adobe.testing.s3mock.dto.ObjectOwnership import com.adobe.testing.s3mock.dto.ObjectOwnership.BUCKET_OWNER_ENFORCED import com.adobe.testing.s3mock.dto.StorageClass import com.adobe.testing.s3mock.dto.Transition +import com.adobe.testing.s3mock.dto.VersioningConfiguration import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -42,28 +51,15 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateBucket() { - val bucket = bucketStore.createBucket( - TEST_BUCKET_NAME, - false, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null - ) + val bucket = givenBucket() + assertThat(bucket.name).endsWith(TEST_BUCKET_NAME) assertThat(bucket.path).exists() } @Test fun testDoesBucketExist_ok() { - bucketStore.createBucket( - TEST_BUCKET_NAME, - false, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null - ) + givenBucket() val doesBucketExist = bucketStore.doesBucketExist(TEST_BUCKET_NAME) @@ -83,30 +79,9 @@ internal class BucketStoreTest : StoreTestBase() { val bucketName2 = "myNüwNämeZwöei" val bucketName3 = "myNüwNämeDrü" - bucketStore.createBucket( - bucketName1, - false, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null - ) - bucketStore.createBucket( - bucketName2, - false, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null - ) - bucketStore.createBucket( - bucketName3, - false, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null - ) + givenBucket(bucketName1) + givenBucket(bucketName2) + givenBucket(bucketName3) val buckets = bucketStore.listBuckets() @@ -115,14 +90,8 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateAndGetBucket() { - bucketStore.createBucket( - TEST_BUCKET_NAME, - false, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null - ) + givenBucket() + val bucket = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) assertThat(bucket).isNotNull() @@ -131,14 +100,10 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateAndGetBucketWithObjectLock() { - bucketStore.createBucket( - TEST_BUCKET_NAME, - true, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null + givenBucket( + objectLockEnabled = true, ) + val bucket = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) assertThat(bucket).isNotNull() @@ -150,14 +115,7 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testStoreAndGetBucketLifecycleConfiguration() { - bucketStore.createBucket( - TEST_BUCKET_NAME, - true, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null - ) + givenBucket() val filter1 = LifecycleRuleFilter(null, null, "documents/", null, null) val transition1 = Transition(null, 30, StorageClass.GLACIER) @@ -176,14 +134,8 @@ internal class BucketStoreTest : StoreTestBase() { @Test fun testCreateAndDeleteBucket() { - bucketStore.createBucket( - TEST_BUCKET_NAME, - false, - BUCKET_OWNER_ENFORCED, - "us-east-1", - null, - null, - ) + givenBucket() + val bucketDeleted = bucketStore.deleteBucket(TEST_BUCKET_NAME) val bucket = bucketStore.doesBucketExist(TEST_BUCKET_NAME) @@ -191,12 +143,158 @@ internal class BucketStoreTest : StoreTestBase() { assertThat(bucket).isFalse } + @Test + fun testIsBucketEmpty_onNewAndAfterAddingKey() { + givenBucket() + + // Newly created bucket should be empty + assertThat(bucketStore.isBucketEmpty(TEST_BUCKET_NAME)).isTrue() + + // Add a key and verify bucket is no longer empty + bucketStore.addKeyToBucket("folder/file.txt", TEST_BUCKET_NAME) + assertThat(bucketStore.isBucketEmpty(TEST_BUCKET_NAME)).isFalse() + } + + @Test + fun testAddLookupRemoveKeys_withAndWithoutPrefix() { + givenBucket() + + val id1 = bucketStore.addKeyToBucket("a/1.txt", TEST_BUCKET_NAME) + val id2 = bucketStore.addKeyToBucket("a/2.txt", TEST_BUCKET_NAME) + val id3 = bucketStore.addKeyToBucket("b/1.txt", TEST_BUCKET_NAME) + + // lookup with null prefix -> all keys + val all = bucketStore.lookupIdsInBucket(null, TEST_BUCKET_NAME) + assertThat(all).containsExactlyInAnyOrder(id1, id2, id3) + + // lookup for prefix "a/" -> two keys + val aOnly = bucketStore.lookupIdsInBucket("a/", TEST_BUCKET_NAME) + assertThat(aOnly).containsExactlyInAnyOrder(id1, id2) + + // lookup for non-matching prefix + val none = bucketStore.lookupIdsInBucket("c/", TEST_BUCKET_NAME) + assertThat(none).isEmpty() + + // remove key and verify behavior + val removed = bucketStore.removeFromBucket("a/1.txt", TEST_BUCKET_NAME) + assertThat(removed).isTrue() + val removedAgain = bucketStore.removeFromBucket("a/1.txt", TEST_BUCKET_NAME) + assertThat(removedAgain).isFalse() + } + + @Test + fun testDeleteBucket_nonEmptyReturnsFalseAndNotDeleted() { + givenBucket() + + bucketStore.addKeyToBucket("keep/me.txt", TEST_BUCKET_NAME) + val deleted = bucketStore.deleteBucket(TEST_BUCKET_NAME) + + assertThat(deleted).isFalse() + assertThat(bucketStore.doesBucketExist(TEST_BUCKET_NAME)).isTrue() + } + + @Test + fun testObjectLockEnabledFlagAndStoringConfiguration() { + // Create without object lock -> disabled + givenBucket() + + assertThat(bucketStore.isObjectLockEnabled(TEST_BUCKET_NAME)).isFalse() + + // Store configuration with ENABLED and verify + val meta = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) + bucketStore.storeObjectLockConfiguration( + meta, + ObjectLockConfiguration(ObjectLockEnabled.ENABLED, null) + ) + assertThat(bucketStore.isObjectLockEnabled(TEST_BUCKET_NAME)).isTrue() + } + + @Test + fun testVersioningConfiguration_enabledAndSuspendedFlags() { + givenBucket() + + var meta = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) + + // enable versioning + bucketStore.storeVersioningConfiguration( + meta, + VersioningConfiguration(null, VersioningConfiguration.Status.ENABLED, null) + ) + meta = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) + assertThat(meta.isVersioningEnabled).isTrue() + assertThat(meta.isVersioningSuspended).isFalse() + + // suspend versioning + bucketStore.storeVersioningConfiguration( + meta, + VersioningConfiguration(null, VersioningConfiguration.Status.SUSPENDED, null) + ) + meta = bucketStore.getBucketMetadata(TEST_BUCKET_NAME) + assertThat(meta.isVersioningEnabled).isFalse() + assertThat(meta.isVersioningSuspended).isTrue() + } + + @Test + fun testCreateBucket_withCustomRegionBucketInfoAndLocationInfo() { + val region = "eu-west-1" + val bucketInfo = BucketInfo(DataRedundancy.SINGLE_AVAILABILITY_ZONE, BucketType.DIRECTORY) + val locationInfo = LocationInfo("eu-west-1a", LocationType.AVAILABILITY_ZONE) + + val bucket = givenBucket( + region = region, + bucketInfo = bucketInfo, + locationInfo = locationInfo, + ) + + assertThat(bucket.bucketRegion).isEqualTo(region) + assertThat(bucket.objectOwnership).isEqualTo(BUCKET_OWNER_ENFORCED) + assertThat(bucket.bucketInfo).isEqualTo(bucketInfo) + assertThat(bucket.locationInfo).isEqualTo(locationInfo) + } + + @Test + fun testIsBucketEmpty_nonExistingBucketThrows() { + assertThatThrownBy { + bucketStore.isBucketEmpty("does-not-exist") + }.isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun testLoadBuckets_returnsExistingObjectIds() { + givenBucket() + + val id1 = bucketStore.addKeyToBucket("x/1", TEST_BUCKET_NAME) + val id2 = bucketStore.addKeyToBucket("x/2", TEST_BUCKET_NAME) + + val loaded = bucketStore.loadBuckets(listOf(TEST_BUCKET_NAME)) + assertThat(loaded).contains(id1, id2) + } + + fun givenBucket( + bucketName: String = TEST_BUCKET_NAME, + objectLockEnabled: Boolean = false, + objectOwnership: ObjectOwnership = BUCKET_OWNER_ENFORCED, + region: String = "us-east-1", + bucketInfo: BucketInfo? = null, + locationInfo: LocationInfo? = null, + ) = bucketStore.createBucket( + bucketName, + objectLockEnabled, + objectOwnership, + region, + bucketInfo, + locationInfo + ) + /** * Delete all existing buckets. */ @AfterEach fun cleanupStores() { for (bucket in bucketStore.listBuckets()) { + bucketStore.lookupKeysInBucket(null, bucket.name).forEach { + bucketStore.removeFromBucket(it, bucket.name) + } bucketStore.deleteBucket(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 e91647757..828a36f9d 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 @@ -448,6 +448,66 @@ internal class MultipartStoreTest : StoreTestBase() { ) } + @Test + @Throws(IOException::class) + fun `MultipartUpload fails when overall checksum does not match`() { + val fileName = "PartFile" + val id = managedId() + val part1 = "Part1" + val part2 = "Part2" + val tempFile1 = Files.createTempFile("", "") + ByteArrayInputStream(part1.toByteArray()).transferTo(Files.newOutputStream(tempFile1)) + val tempFile2 = Files.createTempFile("", "") + ByteArrayInputStream(part2.toByteArray()).transferTo(Files.newOutputStream(tempFile2)) + + val checksumAlgorithm = ChecksumAlgorithm.CRC32 + val checksum1 = DigestUtil.checksumFor(tempFile1, DefaultChecksumAlgorithm.CRC32) + val checksum2 = DigestUtil.checksumFor(tempFile2, DefaultChecksumAlgorithm.CRC32) + + val bucket = metadataFrom(TEST_BUCKET_NAME) + val multipartUpload = multipartStore.createMultipartUpload( + bucket, + fileName, + id, + DEFAULT_CONTENT_TYPE, + storeHeaders(), + TEST_OWNER, + TEST_OWNER, + NO_USER_METADATA, + NO_ENCRYPTION_HEADERS, + NO_TAGS, + StorageClass.STANDARD, + ChecksumType.COMPOSITE, + checksumAlgorithm, + ) + val uploadId = UUID.fromString(multipartUpload.uploadId) + val multipartUploadInfo = multipartStore.getMultipartUploadInfo(bucket, uploadId) + + multipartStore.putPart(bucket, id, uploadId, "1", tempFile1, NO_ENCRYPTION_HEADERS) + multipartStore.putPart(bucket, id, uploadId, "2", tempFile2, NO_ENCRYPTION_HEADERS) + + // Provide wrong overall checksum to trigger verification failure + val wrongOverallChecksum = "AAAAAAAA" // invalid CRC32 base64 + + assertThatThrownBy { + multipartStore.completeMultipartUpload( + bucket, + fileName, + id, + uploadId, + listOf( + CompletedPart(checksum1, null, null, null, null, null, 1), + CompletedPart(checksum2, null, null, null, null, null, 2), + ), + NO_ENCRYPTION_HEADERS, + multipartUploadInfo, + "location", + wrongOverallChecksum, + ChecksumAlgorithm.CRC32, + ) + }.isInstanceOf(S3Exception::class.java) + } + @Test @Throws(IOException::class) fun `MultipartUpload creates an object with checksums in S3Mock, missing checksum in completeMultipartUpload`() { @@ -1093,11 +1153,9 @@ internal class MultipartStoreTest : StoreTestBase() { fun cleanupStores() { arrayListOf().apply { for (id in idCache) { - objectStore.deleteObject(metadataFrom(TEST_BUCKET_NAME), id, null) - objectStore.deleteObject(metadataFrom("bucket1"), id, null) - objectStore.deleteObject(metadataFrom("bucket2"), id, null) - objectStore.deleteObject(metadataFrom("destinationBucket"), id, null) - objectStore.deleteObject(metadataFrom("sourceBucket"), id, null) + BUCKET_NAMES.forEach { + objectStore.deleteObject(metadataFrom(it), id, null) + } this.add(id) } }.also { @@ -1105,6 +1163,13 @@ internal class MultipartStoreTest : StoreTestBase() { idCache.remove(id) } } + + BUCKET_NAMES.forEach { bucket -> + val bucketMetadata = metadataFrom(bucket) + multipartStore.listMultipartUploads(bucketMetadata, NO_PREFIX).forEach { + multipartStore.abortMultipartUpload(bucketMetadata, UUID.randomUUID(), UUID.fromString(it.uploadId)) + } + } } companion object { 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 80c6ef010..4a8199b3b 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 @@ -15,8 +15,10 @@ */ package com.adobe.testing.s3mock.store +import com.adobe.testing.s3mock.S3Exception import com.adobe.testing.s3mock.dto.AccessControlPolicy import com.adobe.testing.s3mock.dto.CanonicalUser +import com.adobe.testing.s3mock.dto.ChecksumAlgorithm import com.adobe.testing.s3mock.dto.ChecksumType import com.adobe.testing.s3mock.dto.Grant import com.adobe.testing.s3mock.dto.LegalHold @@ -27,6 +29,7 @@ import com.adobe.testing.s3mock.dto.StorageClass import com.adobe.testing.s3mock.dto.Tag import com.adobe.testing.s3mock.util.DigestUtil import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -41,6 +44,7 @@ import org.springframework.http.HttpHeaders import org.springframework.test.context.bean.override.mockito.MockitoBean import java.io.File import java.nio.file.Files +import java.nio.file.Path import java.time.Instant import java.time.temporal.ChronoUnit import java.util.Collections @@ -68,11 +72,11 @@ internal class ObjectStoreTest : StoreTestBase() { val name = sourceFile.name val path = sourceFile.toPath() - objectStore.storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, name, null, - storeHeaders(), path, - emptyMap(), emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT + givenStoredS3ObjectMetadata( + id, + name, + path, + contentType = null, ).also { assertThat(it.key).isEqualTo(name) assertThat(it.contentType).isEqualTo(DEFAULT_CONTENT_TYPE) @@ -93,13 +97,12 @@ internal class ObjectStoreTest : StoreTestBase() { val id = managedId() val name = sourceFile.name - objectStore - .storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, name, TEXT_PLAIN, storeHeaders(), - path, - emptyMap(), emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.DEEP_ARCHIVE, ChecksumType.FULL_OBJECT - ) + givenStoredS3ObjectMetadata( + id, + name, + path, + storageClass = StorageClass.DEEP_ARCHIVE, + ) objectStore.getS3ObjectMetadata(metadataFrom(TEST_BUCKET_NAME), id, null).also { assertThat(it!!.key).isEqualTo(name) @@ -122,13 +125,11 @@ internal class ObjectStoreTest : StoreTestBase() { val id = managedId() val name = "/app/config/" + sourceFile.name - objectStore - .storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, name, TEXT_PLAIN, storeHeaders(), - path, - emptyMap(), emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT - ) + givenStoredS3ObjectMetadata( + id, + name, + path, + ) objectStore.getS3ObjectMetadata(metadataFrom(TEST_BUCKET_NAME), id, null).also { assertThat(it!!.key).isEqualTo(name) @@ -145,17 +146,17 @@ internal class ObjectStoreTest : StoreTestBase() { @Test fun testStoreAndGetObjectWithTags() { val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() val id = managedId() val name = sourceFile.name val tags = listOf(Tag("foo", "bar")) - objectStore.storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, name, TEXT_PLAIN, - storeHeaders(), sourceFile.toPath(), - NO_USER_METADATA, emptyMap(), null, tags, null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT + givenStoredS3ObjectMetadata( + id, + name, + path, + tags = tags, ) - objectStore.getS3ObjectMetadata(metadataFrom(TEST_BUCKET_NAME), id, null).also { assertThat(it!!.tags?.get(0)?.key).isEqualTo("foo") assertThat(it.tags?.get(0)?.value).isEqualTo("bar") @@ -165,14 +166,14 @@ internal class ObjectStoreTest : StoreTestBase() { @Test fun testStoreAndGetTagsOnExistingObject() { val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() val id = managedId() val name = sourceFile.name - objectStore.storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, name, TEXT_PLAIN, - storeHeaders(), sourceFile.toPath(), - NO_USER_METADATA, emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT + givenStoredS3ObjectMetadata( + id, + name, + path, ) objectStore.storeObjectTags(metadataFrom(TEST_BUCKET_NAME), id, null, listOf(Tag("foo", "bar"))) @@ -185,14 +186,14 @@ internal class ObjectStoreTest : StoreTestBase() { @Test fun testStoreAndGetRetentionOnExistingObject() { val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() val id = managedId() val name = sourceFile.name - objectStore.storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, name, TEXT_PLAIN, - storeHeaders(), sourceFile.toPath(), - NO_USER_METADATA, emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT + givenStoredS3ObjectMetadata( + id, + name, + path, ) //TODO: resolution of time seems to matter here. Is this a serialization problem? @@ -211,14 +212,14 @@ internal class ObjectStoreTest : StoreTestBase() { @Test fun testStoreAndGetLegalHoldOnExistingObject() { val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() val id = managedId() val name = sourceFile.name - objectStore.storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, name, TEXT_PLAIN, - storeHeaders(), sourceFile.toPath(), - NO_USER_METADATA, emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT + givenStoredS3ObjectMetadata( + id, + name, + path, ) val legalHold = LegalHold(LegalHold.Status.ON) @@ -237,21 +238,31 @@ internal class ObjectStoreTest : StoreTestBase() { val sourceId = managedId() val destinationId = managedId() val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() val sourceBucketName = "sourceBucket" val sourceObjectName = sourceFile.name + val sourceBucketMetadata = metadataFrom(sourceBucketName) - objectStore.storeS3ObjectMetadata( - metadataFrom(sourceBucketName), sourceId, sourceObjectName, - TEXT_PLAIN, storeHeaders(), sourceFile.toPath(), - NO_USER_METADATA, emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.GLACIER, ChecksumType.FULL_OBJECT + givenStoredS3ObjectMetadata( + sourceId, + sourceObjectName, + path, + bucketMetadata = sourceBucketMetadata, + storageClass = StorageClass.GLACIER, ) objectStore.copyS3Object( - metadataFrom(sourceBucketName), sourceId, null, + sourceBucketMetadata, + sourceId, + null, metadataFrom(destinationBucketName), - destinationId, destinationObjectName, emptyMap(), emptyMap(), NO_USER_METADATA, StorageClass.STANDARD_IA + destinationId, + destinationObjectName, + emptyMap(), + emptyMap(), + NO_USER_METADATA, + StorageClass.STANDARD_IA ) objectStore.getS3ObjectMetadata(metadataFrom(destinationBucketName), destinationId, null).also { @@ -274,16 +285,17 @@ internal class ObjectStoreTest : StoreTestBase() { val sourceBucketName = "sourceBucket" val sourceObjectName = sourceFile.name + val sourceBucketMetadata = metadataFrom(sourceBucketName) - objectStore.storeS3ObjectMetadata( - metadataFrom(sourceBucketName), sourceId, sourceObjectName, - TEXT_PLAIN, storeHeaders(), path, - NO_USER_METADATA, emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT + givenStoredS3ObjectMetadata( + sourceId, + sourceObjectName, + path, + bucketMetadata = sourceBucketMetadata, ) objectStore.copyS3Object( - metadataFrom(sourceBucketName), + sourceBucketMetadata, sourceId, null, metadataFrom(destinationBucketName), @@ -299,24 +311,22 @@ internal class ObjectStoreTest : StoreTestBase() { assertThat(it.size).isEqualTo(sourceFile.length().toString()) assertThat(it.etag).isEqualTo("\"${DigestUtil.hexDigest(TEST_ENC_KEY, Files.newInputStream(path))}\"") } - } @Test fun testStoreAndDeleteObject() { val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() val id = managedId() - val objectName = sourceFile.name - - objectStore - .storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, objectName, TEXT_PLAIN, - storeHeaders(), sourceFile.toPath(), - NO_USER_METADATA, emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT - ) - objectStore.deleteObject(metadataFrom(TEST_BUCKET_NAME), id, null).also { + val name = sourceFile.name + + givenStoredS3ObjectMetadata( + id, + name, + path, + ) + objectStore.deleteObject(metadataFrom(TEST_BUCKET_NAME), id, null).also { assertThat(it).isTrue() } objectStore.getS3ObjectMetadata(metadataFrom(TEST_BUCKET_NAME), id, null).also { @@ -338,15 +348,16 @@ internal class ObjectStoreTest : StoreTestBase() { ) val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() val id = managedId() - val objectName = sourceFile.name - objectStore - .storeS3ObjectMetadata( - metadataFrom(TEST_BUCKET_NAME), id, objectName, TEXT_PLAIN, - storeHeaders(), sourceFile.toPath(), - NO_USER_METADATA, emptyMap(), null, emptyList(), null, null, Owner.DEFAULT_OWNER, - StorageClass.STANDARD, ChecksumType.FULL_OBJECT - ) + val name = sourceFile.name + + givenStoredS3ObjectMetadata( + id, + name, + path, + ) + val bucket = metadataFrom(TEST_BUCKET_NAME) objectStore.storeAcl(bucket, id, null, policy) @@ -355,6 +366,140 @@ internal class ObjectStoreTest : StoreTestBase() { assertThat(actual).isEqualTo(policy) } + @Test + fun pretendToCopy_requires_a_change() { + val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() + val id = managedId() + val name = sourceFile.name + val bucket = metadataFrom("bucket-pretend") + + givenStoredS3ObjectMetadata( + id, + name, + path, + bucketMetadata = bucket, + ) + + // All optional params unchanged -> should throw INVALID_COPY_REQUEST_SAME_KEY + assertThatThrownBy { + objectStore.pretendToCopyS3Object( + bucket, + id, + null, + null, + null, + null, + null + ) + }.isSameAs(S3Exception.INVALID_COPY_REQUEST_SAME_KEY) + } + + @Test + fun pretendToCopy_with_changes_updates_metadata_and_persists() { + val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() + val id = managedId() + val name = sourceFile.name + val bucket = metadataFrom("bucket-pretend") + + givenStoredS3ObjectMetadata( + id, + name, + path, + bucketMetadata = bucket, + ) + + val newUserMetadata = mapOf("x-amz-meta-key" to "value") + val newStoreHeaders = mapOf("cache-control" to "no-cache") + val newEncHeaders = encryptionHeaders() + val newStorageClass = StorageClass.STANDARD_IA + + val updated = objectStore.pretendToCopyS3Object( + bucket, + id, + null, + newEncHeaders, + newStoreHeaders, + newUserMetadata, + newStorageClass + )!! + + // Verify fields changed accordingly + assertThat(updated.userMetadata).isEqualTo(newUserMetadata) + assertThat(updated.storeHeaders).isEqualTo(newStoreHeaders) + assertThat(updated.encryptionHeaders).isEqualTo(newEncHeaders) + assertThat(updated.storageClass).isEqualTo(newStorageClass) + + // And persisted to disk (re-read) + val reread = objectStore.getS3ObjectMetadata(bucket, id, null)!! + assertThat(reread.userMetadata).isEqualTo(newUserMetadata) + assertThat(reread.storeHeaders).isEqualTo(newStoreHeaders) + assertThat(reread.encryptionHeaders).isEqualTo(newEncHeaders) + assertThat(reread.storageClass).isEqualTo(newStorageClass) + } + + @Test + fun store_with_checksum_fields_persists_checksum() { + val sourceFile = File(TEST_FILE_PATH) + val path = sourceFile.toPath() + val id = managedId() + val name = sourceFile.name + val bucket = metadataFrom(TEST_BUCKET_NAME) + + val algo = ChecksumAlgorithm.SHA256 + + val metadata = givenStoredS3ObjectMetadata( + id, + name, + path, + bucketMetadata = bucket, + checksumAlgorithm = algo, + checksumValue = "dummy-checksum", // We don't validate here; just persist + ) + + assertThat(metadata.checksumAlgorithm).isEqualTo(algo) + assertThat(metadata.checksum).isEqualTo("dummy-checksum") + + val reread = objectStore.getS3ObjectMetadata(bucket, id, null)!! + assertThat(reread.checksumAlgorithm).isEqualTo(algo) + assertThat(reread.checksum).isEqualTo("dummy-checksum") + } + + private fun givenStoredS3ObjectMetadata( + id: UUID, + name: String, + path: Path, + bucketMetadata: BucketMetadata = metadataFrom(TEST_BUCKET_NAME), + contentType: String? = TEXT_PLAIN, + storeHeaders: Map = storeHeaders(), + userMetadata: Map = NO_USER_METADATA, + encryptionHeaders: Map = emptyMap(), + etag: String? = null, + tags: List? = emptyList(), + checksumAlgorithm: ChecksumAlgorithm? = null, + checksumValue: String? = null, + owner: Owner = Owner.DEFAULT_OWNER, + storageClass: StorageClass = StorageClass.STANDARD, + checksumType: ChecksumType = ChecksumType.FULL_OBJECT + ): S3ObjectMetadata = objectStore.storeS3ObjectMetadata( + bucketMetadata, + id, + name, + contentType, + storeHeaders, + path, + userMetadata, + encryptionHeaders, + etag, + tags, + checksumAlgorithm, + checksumValue, + owner, + storageClass, + checksumType + ) + private fun managedId(): UUID { val uuid = UUID.randomUUID() idCache.add(uuid) 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 39df5afb4..9a4154c1c 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 @@ -36,6 +36,7 @@ internal abstract class StoreTestBase { private val rootFolder: File? = null protected fun metadataFrom(bucketName: String): BucketMetadata { + BUCKET_NAMES.add(bucketName) return BucketMetadata( bucketName, Date().toString(), @@ -84,5 +85,7 @@ internal abstract class StoreTestBase { val NO_PREFIX: String? = null const val DEFAULT_CONTENT_TYPE: String = MediaType.APPLICATION_OCTET_STREAM_VALUE val TEST_OWNER: Owner = Owner("s3-mock-file-store", "123") + + val BUCKET_NAMES = mutableSetOf() } }