Skip to content

Commit 9b31402

Browse files
committed
API consistency: Multipart API / DTOs
Checking AWS APIs for changes and S3Mock for implementations and tests. Fixes #2340
1 parent e1cb3cd commit 9b31402

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1762
-689
lines changed

CHANGELOG.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,20 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
150150
* Check AWS API for changes
151151
* Update S3Mock API / DTOs
152152
* Add tests for changed API / DTOs
153-
* List Objects now returns "delimiter"
154-
* List Objects V2 now accepts "fetch-owner" and returns "delimiter"
155-
* List Buckets now accepts all parameters listed in AWS S3 API
153+
* CreateBucket API now accepts "CreateBucketConfiguration" request body
154+
* CompleteMultipartUpload API now accepts checksums and returns checksums
155+
* ListObjects API now returns "delimiter"
156+
* ListObjects V2 API now accepts "fetch-owner" and returns "delimiter"
157+
* ListBuckets API now accepts parameters listed in AWS S3 API
158+
* CreateMultipartUpload now accepts checksum headers and returns checksum and encryption headers
159+
* CompleteMultipartUpload now accepts checksum headers and returns checksum and encryption headers
160+
* Checksum validation on complete
156161
* Version updates (deliverable dependencies)
157162
* Bump spring-boot.version from 3.4.4 to 3.4.5
158163
* Bump testcontainers.version from 1.20.6 to 1.21.0
159164
* Version updates (build dependencies)
160-
* Bump github/codeql-action from 3.28.15 to 3.28.16
165+
* Bump github/codeql-action from 3.28.15 to 3.28.17
166+
* Bump com.puppycrawl.tools:checkstyle from 10.23.0 to 10.23.1
161167

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

integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/MultiPartIT.kt

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import software.amazon.awssdk.services.s3.S3AsyncClient
3737
import software.amazon.awssdk.services.s3.S3Client
3838
import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm
3939
import software.amazon.awssdk.services.s3.model.ChecksumMode
40+
import software.amazon.awssdk.services.s3.model.ChecksumType
41+
import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest
4042
import software.amazon.awssdk.services.s3.model.CompletedPart
4143
import software.amazon.awssdk.services.s3.model.ListPartsRequest
4244
import software.amazon.awssdk.services.s3.model.NoSuchBucketException
@@ -255,13 +257,9 @@ internal class MultiPartIT : S3TestBase() {
255257
.isEqualTo("${serviceEndpoint}/$bucketName/${UriUtils.encode(UPLOAD_FILE_NAME, StandardCharsets.UTF_8)}")
256258
}
257259

258-
259-
/**
260-
* Tests if a multipart upload with the last part being smaller than 5MB works.
261-
*/
262260
@Test
263261
@S3VerifiedSuccess(year = 2025)
264-
fun testMultipartUpload_checksum(testInfo: TestInfo) {
262+
fun `multipartupload send checksum in create and complete`(testInfo: TestInfo) {
265263
val bucketName = givenBucket(testInfo)
266264
val uploadFile = File(TEST_IMAGE_TIFF)
267265
//construct uploadfile >5MB
@@ -277,7 +275,10 @@ internal class MultiPartIT : S3TestBase() {
277275
it.bucket(bucketName)
278276
it.key(TEST_IMAGE_TIFF)
279277
it.checksumAlgorithm(ChecksumAlgorithm.CRC32)
278+
it.checksumType(ChecksumType.COMPOSITE)
280279
}
280+
assertThat(initiateMultipartUploadResult.checksumAlgorithm()).isEqualTo(ChecksumAlgorithm.CRC32)
281+
assertThat(initiateMultipartUploadResult.checksumType()).isEqualTo(ChecksumType.COMPOSITE)
281282
val uploadId = initiateMultipartUploadResult.uploadId()
282283
// upload part 1, <5MB
283284
val partResponse1 = s3Client.uploadPart(
@@ -289,7 +290,6 @@ internal class MultiPartIT : S3TestBase() {
289290
it.partNumber(1)
290291
it.contentLength(tempFile.length())
291292
},
292-
//.lastPart(true)
293293
RequestBody.fromFile(tempFile),
294294
)
295295
val etag1 = partResponse1.eTag()
@@ -303,7 +303,6 @@ internal class MultiPartIT : S3TestBase() {
303303
it.partNumber(2)
304304
it.contentLength(uploadFile.length())
305305
},
306-
//.lastPart(true)
307306
RequestBody.fromFile(uploadFile),
308307
)
309308
val etag2 = partResponse2.eTag()
@@ -354,11 +353,87 @@ internal class MultiPartIT : S3TestBase() {
354353
.isEqualTo("${serviceEndpoint}/$bucketName/${UriUtils.encode(TEST_IMAGE_TIFF, StandardCharsets.UTF_8)}")
355354
}
356355

356+
@Test
357+
@S3VerifiedSuccess(year = 2025)
358+
fun `multipartupload send checksum in create only`(testInfo: TestInfo) {
359+
val bucketName = givenBucket(testInfo)
360+
val uploadFile = File(TEST_IMAGE_TIFF)
361+
//construct uploadfile >5MB
362+
val tempFile = Files.newTemporaryFile().also {
363+
(readStreamIntoByteArray(uploadFile.inputStream()) +
364+
readStreamIntoByteArray(uploadFile.inputStream()) +
365+
readStreamIntoByteArray(uploadFile.inputStream()))
366+
.inputStream()
367+
.copyTo(it.outputStream())
368+
}
369+
370+
val initiateMultipartUploadResult = s3Client.createMultipartUpload {
371+
it.bucket(bucketName)
372+
it.key(TEST_IMAGE_TIFF)
373+
it.checksumAlgorithm(ChecksumAlgorithm.CRC32)
374+
}
375+
val uploadId = initiateMultipartUploadResult.uploadId()
376+
// upload part 1, <5MB
377+
val partResponse1 = s3Client.uploadPart(
378+
{
379+
it.bucket(initiateMultipartUploadResult.bucket())
380+
it.key(initiateMultipartUploadResult.key())
381+
it.uploadId(uploadId)
382+
it.checksumAlgorithm(ChecksumAlgorithm.CRC32)
383+
it.partNumber(1)
384+
it.contentLength(tempFile.length())
385+
},
386+
RequestBody.fromFile(tempFile),
387+
)
388+
val etag1 = partResponse1.eTag()
389+
val checksum1 = partResponse1.checksumCRC32()
390+
// upload part 2, <5MB
391+
val partResponse2 = s3Client.uploadPart({
392+
it.bucket(initiateMultipartUploadResult.bucket())
393+
it.key(initiateMultipartUploadResult.key())
394+
it.uploadId(uploadId)
395+
it.checksumAlgorithm(ChecksumAlgorithm.CRC32)
396+
it.partNumber(2)
397+
it.contentLength(uploadFile.length())
398+
},
399+
RequestBody.fromFile(uploadFile),
400+
)
401+
val etag2 = partResponse2.eTag()
402+
val checksum2 = partResponse2.checksumCRC32()
403+
val localChecksum1 = DigestUtil.checksumFor(tempFile.toPath(), DefaultChecksumAlgorithm.CRC32)
404+
assertThat(checksum1).isEqualTo(localChecksum1)
405+
val localChecksum2 = DigestUtil.checksumFor(uploadFile.toPath(), DefaultChecksumAlgorithm.CRC32)
406+
assertThat(checksum2).isEqualTo(localChecksum2)
407+
408+
assertThatThrownBy {
409+
s3Client.completeMultipartUpload {
410+
it.bucket(initiateMultipartUploadResult.bucket())
411+
it.key(initiateMultipartUploadResult.key())
412+
it.uploadId(initiateMultipartUploadResult.uploadId())
413+
it.multipartUpload {
414+
it.parts(
415+
{
416+
it.eTag(etag1)
417+
it.partNumber(1)
418+
},
419+
{
420+
it.eTag(etag2)
421+
it.partNumber(2)
422+
}
423+
)
424+
}
425+
}
426+
}.isInstanceOf(S3Exception::class.java)
427+
.hasMessageContaining("Service: S3, Status Code: 400")
428+
.hasMessageContaining("The upload was created using a crc32 checksum. The complete request must include the " +
429+
"checksum for each part. It was missing for part 1 in the request.")
430+
}
431+
357432

358433
@S3VerifiedSuccess(year = 2025)
359434
@ParameterizedTest
360435
@MethodSource(value = ["checksumAlgorithms"])
361-
fun testUploadPart_checksumAlgorithm(checksumAlgorithm: software.amazon.awssdk.checksums.spi.ChecksumAlgorithm,
436+
fun testUploadPart_checksumAlgorithm_initiate(checksumAlgorithm: software.amazon.awssdk.checksums.spi.ChecksumAlgorithm,
362437
testInfo: TestInfo) {
363438
val bucketName = givenBucket(testInfo)
364439
val uploadFile = File(UPLOAD_FILE_NAME)
@@ -377,7 +452,6 @@ internal class MultiPartIT : S3TestBase() {
377452
it.checksumAlgorithm(checksumAlgorithm.toAlgorithm())
378453
it.partNumber(1)
379454
it.contentLength(uploadFile.length()).build()
380-
//.lastPart(true)
381455
},
382456
RequestBody.fromFile(uploadFile),
383457
).also {
@@ -392,6 +466,45 @@ internal class MultiPartIT : S3TestBase() {
392466
}
393467
}
394468

469+
@S3VerifiedSuccess(year = 2025)
470+
@ParameterizedTest
471+
@MethodSource(value = ["checksumAlgorithms"])
472+
fun testUploadPart_checksumAlgorithm_complete(
473+
checksumAlgorithm: software.amazon.awssdk.checksums.spi.ChecksumAlgorithm,
474+
testInfo: TestInfo
475+
) {
476+
val bucketName = givenBucket(testInfo)
477+
val uploadFile = File(UPLOAD_FILE_NAME)
478+
val initiateMultipartUploadResult = s3Client.createMultipartUpload {
479+
it.bucket(bucketName)
480+
it.key(UPLOAD_FILE_NAME)
481+
}
482+
val uploadId = initiateMultipartUploadResult.uploadId()
483+
484+
s3Client.uploadPart({
485+
it.bucket(initiateMultipartUploadResult.bucket())
486+
it.key(initiateMultipartUploadResult.key())
487+
it.uploadId(uploadId)
488+
it.partNumber(1)
489+
it.contentLength(uploadFile.length()).build()
490+
//.lastPart(true)
491+
},
492+
RequestBody.fromFile(uploadFile),
493+
)
494+
495+
assertThatThrownBy {
496+
s3Client.completeMultipartUpload {
497+
it.bucket(bucketName)
498+
it.key(UPLOAD_FILE_NAME)
499+
it.uploadId(uploadId)
500+
it.checksumType(ChecksumType.COMPOSITE)
501+
it.checksum("WRONG CHECKSUM", checksumAlgorithm.toAlgorithm())
502+
}
503+
}
504+
.isInstanceOf(S3Exception::class.java)
505+
.hasMessageContaining("Service: S3, Status Code: 400")
506+
}
507+
395508
@S3VerifiedSuccess(year = 2025)
396509
@ParameterizedTest
397510
@MethodSource(value = ["checksumAlgorithms"])
@@ -473,6 +586,19 @@ internal class MultiPartIT : S3TestBase() {
473586
else -> error("Unknown checksum algorithm")
474587
}
475588

589+
private fun CompleteMultipartUploadRequest.Builder.checksum(
590+
checksum: String,
591+
checksumAlgorithm: ChecksumAlgorithm
592+
): CompleteMultipartUploadRequest.Builder =
593+
when (checksumAlgorithm) {
594+
ChecksumAlgorithm.SHA1 -> this.checksumSHA1(checksum)
595+
ChecksumAlgorithm.SHA256 -> this.checksumSHA256(checksum)
596+
ChecksumAlgorithm.CRC32 -> this.checksumCRC32(checksum)
597+
ChecksumAlgorithm.CRC32_C -> this.checksumCRC32C(checksum)
598+
ChecksumAlgorithm.CRC64_NVME -> this.checksumCRC64NVME(checksum)
599+
else -> error("Unknown checksum algorithm")
600+
}
601+
476602
@Test
477603
@S3VerifiedSuccess(year = 2025)
478604
fun testInitiateMultipartAndRetrieveParts(testInfo: TestInfo) {
@@ -1372,6 +1498,6 @@ internal class MultiPartIT : S3TestBase() {
13721498
companion object {
13731499
private const val NO_SUCH_BUCKET = "The specified bucket does not exist"
13741500
private const val INVALID_PART_NUMBER = "Part number must be an integer between 1 and 10000, inclusive"
1375-
private const val INVALID_PART = "One or more of the specified parts could not be found. The part might not have been uploaded, or the specified entity tagSet might not have matched the part's entity tagSet."
1501+
private const val INVALID_PART = "One or more of the specified parts could not be found. The part might not have been uploaded, or the specified entity tag may not match the part's entity tag."
13761502
}
13771503
}

0 commit comments

Comments
 (0)