Skip to content

Commit be6b1ea

Browse files
committed
fix: Return 412 on if-none-match=true in CompleteMultipartRequest
Also fixes links in Kotlin doc and wording in S3 Exception. Fixes #2790
1 parent 541a987 commit be6b1ea

File tree

6 files changed

+65
-13
lines changed

6 files changed

+65
-13
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
152152
* Get object with range now returns the same headers as non-range calls.
153153
* Docker: Copy "s3mock.jar" to "/opt/", run with absolute path reference to avoid issues when working directory is changed. (fixes #2827)
154154
* S3Mock supports ChecksumType.FULL_OBJECT for Multipart uploads (fixes #2843)
155+
* Return 412 on if-none-match=true when making CompleteMultipartRequest (fixes #2790)
155156
* Refactorings
156157
* Use Jackson 3 annotations and mappers.
157158
* AWS has deprecated SDK for Java v1 and will remove support EOY 2025.
@@ -170,7 +171,10 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
170171
* Bump TestContainers to 2.0.2
171172
* Version updates (build dependencies)
172173
* Bump Maven to 4.0.0
173-
* Bump github/codeql-action from 4.31.6 to 4.31.7
174+
* Bump org.apache.maven.plugins:maven-release-plugin from 3.3.0 to 3.3.1
175+
* Bump com.puppycrawl.tools:checkstyle from 12.2.0 to 12.3.0
176+
* Bump actions/upload-artifact from 5.0.0 to 6.0.0
177+
* Bump github/codeql-action from 4.31.6 to 4.31.8
174178
* Bump actions/setup-java from 5.0.0 to 5.1.0
175179
* Bump step-security/harden-runner from 2.13.3 to 2.14.0
176180

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

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ internal class MultipartIT : S3TestBase() {
497497
it.bucket(initiateMultipartUploadResult.bucket())
498498
it.key(initiateMultipartUploadResult.key())
499499
it.uploadId(initiateMultipartUploadResult.uploadId())
500+
it.checksumType(ChecksumType.COMPOSITE)
500501
it.multipartUpload {
501502
it.parts(
502503
{
@@ -541,6 +542,7 @@ internal class MultipartIT : S3TestBase() {
541542
it.bucket(initiateMultipartUploadResult.bucket())
542543
it.key(initiateMultipartUploadResult.key())
543544
it.uploadId(initiateMultipartUploadResult.uploadId())
545+
it.checksumType(ChecksumType.COMPOSITE)
544546
it.multipartUpload {
545547
it.parts(
546548
{
@@ -563,7 +565,6 @@ internal class MultipartIT : S3TestBase() {
563565
assertThat(completeMultipartUpload.bucket()).isEqualTo(completeMultipartUpload1.bucket())
564566
assertThat(completeMultipartUpload.key()).isEqualTo(completeMultipartUpload1.key())
565567
assertThat(completeMultipartUpload.eTag()).isEqualTo(completeMultipartUpload1.eTag())
566-
assertThat(completeMultipartUpload.checksumCRC32()).isEqualTo(completeMultipartUpload1.checksumCRC32())
567568
assertThat(completeMultipartUpload.checksumType()).isEqualTo(completeMultipartUpload1.checksumType())
568569
}
569570

@@ -1812,7 +1813,7 @@ internal class MultipartIT : S3TestBase() {
18121813
it.sourceKey(sourceKey)
18131814
it.sourceBucket(bucketName)
18141815
it.partNumber(1)
1815-
it.copySourceRange("bytes=0-$UPLOAD_FILE_LENGTH")
1816+
it.copySourceRange("bytes=0-${UPLOAD_FILE_LENGTH - 1}")
18161817
it.copySourceIfModifiedSince(now)
18171818
}
18181819
}.isInstanceOf(S3Exception::class.java)
@@ -2081,6 +2082,54 @@ internal class MultipartIT : S3TestBase() {
20812082
.hasMessageContaining(INVALID_PART)
20822083
}
20832084

2085+
@Test
2086+
@S3VerifiedSuccess(year = 2025)
2087+
fun `CompleteMultipart fails with if-none-match=true`(testInfo: TestInfo) {
2088+
val (bucketName, response) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
2089+
val initiateMultipartUploadResult =
2090+
s3Client
2091+
.createMultipartUpload {
2092+
it.bucket(bucketName)
2093+
it.key(UPLOAD_FILE_NAME)
2094+
}
2095+
val uploadId = initiateMultipartUploadResult.uploadId()
2096+
2097+
assertThat(
2098+
s3Client
2099+
.listMultipartUploads {
2100+
it.bucket(bucketName)
2101+
}.uploads(),
2102+
).isNotEmpty
2103+
2104+
val eTag =
2105+
s3Client
2106+
.uploadPart(
2107+
{
2108+
it.bucket(initiateMultipartUploadResult.bucket())
2109+
it.key(initiateMultipartUploadResult.key())
2110+
it.uploadId(uploadId)
2111+
it.partNumber(1)
2112+
},
2113+
RequestBody.fromFile(UPLOAD_FILE),
2114+
).eTag()
2115+
2116+
assertThatThrownBy {
2117+
s3Client.completeMultipartUpload {
2118+
it.bucket(initiateMultipartUploadResult.bucket())
2119+
it.key(initiateMultipartUploadResult.key())
2120+
it.uploadId(uploadId)
2121+
it.ifNoneMatch("*")
2122+
it.multipartUpload {
2123+
it.parts({
2124+
it.eTag(eTag)
2125+
it.partNumber(1)
2126+
})
2127+
}
2128+
}
2129+
}.isInstanceOf(S3Exception::class.java)
2130+
.hasMessageContaining(PRECONDITION_FAILED.message)
2131+
}
2132+
20842133
private fun uploadPart(
20852134
bucketName: String,
20862135
key: String,
@@ -2107,7 +2156,7 @@ internal class MultipartIT : S3TestBase() {
21072156
private const val NO_SUCH_BUCKET = "The specified bucket does not exist"
21082157
private const val INVALID_PART_NUMBER = "Part number must be an integer between 1 and 10000, inclusive"
21092158
private const val INVALID_PART =
2110-
"One or more of the specified parts could not be found. " +
2111-
"The part might not have been uploaded, or the specified entity tag may not match the part's entity tag."
2159+
"One or more of the specified parts could not be found. " +
2160+
"The part may not have been uploaded, or the specified entity tag may not match the part's entity tag."
21122161
}
21132162
}

server/src/main/kotlin/com/adobe/testing/s3mock/S3Exception.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ import org.springframework.http.HttpStatus
2020
/**
2121
* [RuntimeException] to communicate general S3 errors.
2222
* These are handled by ControllerConfiguration.S3MockExceptionHandler,
23-
* mapped to [ErrorResponse] and serialized.
23+
* mapped to [com.adobe.testing.s3mock.dto.ErrorResponse] and serialized.
2424
* [API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html)
2525
*/
2626
class S3Exception
2727
/**
28-
* Creates a new S3Exception to be mapped as an [ErrorResponse].
28+
* Creates a new S3Exception to be mapped as an [com.adobe.testing.s3mock.dto.ErrorResponse].
2929
*
3030
* @param status The Error Status.
3131
* @param code The Error Code.
@@ -43,7 +43,7 @@ class S3Exception
4343
)
4444
val INVALID_PART: S3Exception = S3Exception(
4545
HttpStatus.BAD_REQUEST.value(), "InvalidPart",
46-
"One or more of the specified parts could not be found. The part might not have been "
46+
"One or more of the specified parts could not be found. The part may not have been "
4747
+ "uploaded, or the specified entity tag may not match the part's entity tag."
4848
)
4949
val INVALID_PART_ORDER: S3Exception = S3Exception(

server/src/main/kotlin/com/adobe/testing/s3mock/controller/ChecksumModeHeaderConverter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import com.adobe.testing.s3mock.dto.ChecksumMode
1919
import org.springframework.core.convert.converter.Converter
2020

2121
/**
22-
* Converts values of the [AwsHttpHeaders.X_AMZ_CHECKSUM_MODE] which is sent by the Amazon
22+
* Converts values of the [com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_CHECKSUM_MODE] which is sent by the Amazon
2323
* client.
2424
* Example: x-amz-checksum-mode: ENABLED
2525
* [API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html)

server/src/main/kotlin/com/adobe/testing/s3mock/controller/MultipartController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ class MultipartController(
434434
multipartService.verifyMultipartParts(bucketName, objectName, uploadId, upload.parts)
435435
}
436436
val s3ObjectMetadata = objectService.getObject(bucketName, key.key, null)
437-
objectService.verifyObjectMatching(match, noneMatch, null, null, s3ObjectMetadata)
437+
objectService.verifyObjectMatching(bucketName, key.key, match, noneMatch)
438438
val locationWithEncodedKey = request
439439
.requestURL
440440
.toString()

server/src/test/kotlin/com/adobe/testing/s3mock/controller/MultipartControllerTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,10 @@ internal class MultipartControllerTest : BaseControllerTest() {
475475
doThrow(S3Exception.PRECONDITION_FAILED)
476476
.whenever(objectService)
477477
.verifyObjectMatching(
478+
eq(TEST_BUCKET_NAME),
479+
eq(key),
478480
anyOrNull(),
479481
anyOrNull(),
480-
anyOrNull(),
481-
anyOrNull(),
482-
eq(s3meta)
483482
)
484483

485484
val uri = UriComponentsBuilder

0 commit comments

Comments
 (0)