From 6e9235387bfd18f4c6114c246009591a55a2a21b Mon Sep 17 00:00:00 2001 From: "Veer.Kumar" Date: Tue, 10 Feb 2026 21:22:01 +0000 Subject: [PATCH 1/4] Added comments on all the test cases and removed traceback on test failure --- .../com/datadobi/s3test/ChecksumTests.java | 24 ++++++ .../s3test/ConditionalRequestTests.java | 13 ++++ .../datadobi/s3test/DeleteObjectTests.java | 8 ++ .../datadobi/s3test/DeleteObjectsTests.java | 16 ++++ .../com/datadobi/s3test/GetObjectTests.java | 36 +++++++++ .../com/datadobi/s3test/ListBucketsTests.java | 4 + .../com/datadobi/s3test/ListObjectsTests.java | 76 +++++++++++++++++++ .../datadobi/s3test/MultiPartUploadTests.java | 4 + .../com/datadobi/s3test/ObjectKeyTests.java | 40 ++++++++++ .../datadobi/s3test/PrefixDelimiterTests.java | 32 ++++++++ .../com/datadobi/s3test/PutObjectTests.java | 32 ++++++++ .../java/com/datadobi/s3test/RunTests.java | 24 +++++- 12 files changed, 305 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/datadobi/s3test/ChecksumTests.java b/src/main/java/com/datadobi/s3test/ChecksumTests.java index 03c1851..c74e778 100644 --- a/src/main/java/com/datadobi/s3test/ChecksumTests.java +++ b/src/main/java/com/datadobi/s3test/ChecksumTests.java @@ -37,30 +37,50 @@ public class ChecksumTests extends S3TestBase { public ChecksumTests() throws IOException { } + /** + * Puts an object with CRC32 checksum and verifies the server stores and returns it. + * Expected: Put succeeds; HEAD with ChecksumMode.ENABLED returns same content-length, ETag, and CRC32 checksum. + */ @Test @SkipForQuirks({Quirk.CHECKSUMS_NOT_SUPPORTED}) public void testCRC32() { putWithChecksum(ChecksumAlgorithm.CRC32); } + /** + * Puts an object with CRC32C checksum and verifies the server stores and returns it. + * Expected: Put succeeds; HEAD with ChecksumMode.ENABLED returns same content-length, ETag, and CRC32C checksum. + */ @Test @SkipForQuirks({Quirk.CHECKSUMS_NOT_SUPPORTED}) public void testCRC32_C() { putWithChecksum(ChecksumAlgorithm.CRC32_C); } + /** + * Puts an object with SHA1 checksum and verifies the server stores and returns it. + * Expected: Put succeeds; HEAD with ChecksumMode.ENABLED returns same content-length, ETag, and SHA1 checksum. + */ @Test @SkipForQuirks({Quirk.CHECKSUMS_NOT_SUPPORTED}) public void testSHA1() { putWithChecksum(ChecksumAlgorithm.SHA1); } + /** + * Puts an object with SHA256 checksum and verifies the server stores and returns it. + * Expected: Put succeeds; HEAD with ChecksumMode.ENABLED returns same content-length, ETag, and SHA256 checksum. + */ @Test @SkipForQuirks({Quirk.CHECKSUMS_NOT_SUPPORTED}) public void testSHA256() { putWithChecksum(ChecksumAlgorithm.SHA256); } + /** + * Puts an object with CRC64_NVME checksum and verifies the server stores and returns it. + * Expected: Put succeeds; HEAD returns same CRC64_NVME checksum. Skipped by default (requires C runtime). + */ @Test @SkipForQuirks({Quirk.CHECKSUMS_NOT_SUPPORTED}) @Ignore("Requires C runtime") @@ -99,6 +119,10 @@ private void putWithChecksum(ChecksumAlgorithm checksumAlgorithm) { assertEquals("Checksum mismatch", putChecksum, headChecksum); } + /** + * Puts an object using indefinite-length (chunked) request body with CRC32 checksum. + * Expected: Put completes successfully without requiring a predeclared Content-Length. + */ @Test @SkipForQuirks({Quirk.CHECKSUMS_NOT_SUPPORTED}) public void indefiniteLengthWithChecksum() { diff --git a/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java b/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java index 7616ae1..11fc1d4 100644 --- a/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java +++ b/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java @@ -35,6 +35,11 @@ public class ConditionalRequestTests extends S3TestBase { public ConditionalRequestTests() throws IOException { } + /** + * Puts an object, then attempts overwrite with If-None-Match: * (create-only semantics). + * Expected: First put succeeds; second put either fails with 412 Precondition Failed (object exists) + * or 501 if unsupported; if it wrongly succeeds, GET returns new content and test fails. + */ @Test public void thatConditionalPutIfNoneMatchStarWorks() throws IOException, InterruptedException { bucket.putObject("object", "hello"); @@ -67,6 +72,10 @@ public void thatConditionalPutIfNoneMatchStarWorks() throws IOException, Interru } } + /** + * Puts an object, then attempts overwrite with If-None-Match: "" (create-only if etag differs). + * Expected: Overwrite fails with 412 (or 501 if unsupported); object content remains unchanged. + */ @Test public void thatConditionalPutIfNoneMatchEtagWorks() throws IOException, InterruptedException { var initialPutResponse = bucket.putObject( @@ -102,6 +111,10 @@ public void thatConditionalPutIfNoneMatchEtagWorks() throws IOException, Interru } } + /** + * Puts an object, then overwrites with If-Match: "" (update-only if etag matches). + * Expected: Overwrite succeeds; GET returns new content and new ETag; or 412/501 if precondition fails. + */ @Test public void thatConditionalPutIfMatchEtagWorks() throws IOException, InterruptedException { var initialPutResponse = bucket.putObject("object", "hello"); diff --git a/src/main/java/com/datadobi/s3test/DeleteObjectTests.java b/src/main/java/com/datadobi/s3test/DeleteObjectTests.java index c62baaf..46dbb72 100644 --- a/src/main/java/com/datadobi/s3test/DeleteObjectTests.java +++ b/src/main/java/com/datadobi/s3test/DeleteObjectTests.java @@ -27,12 +27,20 @@ public class DeleteObjectTests extends S3TestBase { public DeleteObjectTests() throws IOException { } + /** + * Puts an object then deletes it with DeleteObject. + * Expected: Delete succeeds; object is no longer present (no exception). + */ @Test public void testDeleteObject() { bucket.putObject("foo", "Hello, World!"); bucket.deleteObject("foo"); } + /** + * Puts and then deletes an object whose key contains ".." (e.g. "f..o"). + * Expected: HEAD confirms object exists; DeleteObject succeeds without error. + */ @Test public void testDeleteObjectContainingDotDot() { var fullContent = "Hello, World!"; diff --git a/src/main/java/com/datadobi/s3test/DeleteObjectsTests.java b/src/main/java/com/datadobi/s3test/DeleteObjectsTests.java index 0eed256..4db9ab1 100644 --- a/src/main/java/com/datadobi/s3test/DeleteObjectsTests.java +++ b/src/main/java/com/datadobi/s3test/DeleteObjectsTests.java @@ -31,6 +31,10 @@ public class DeleteObjectsTests extends S3TestBase { public DeleteObjectsTests() throws IOException { } + /** + * Deletes multiple objects by key in a single DeleteObjects request. + * Expected: Both objects are removed; response hasDeleted() is true and deleted list contains "a" and "b". + */ @Test public void testDeleteObjectsByKey() { bucket.putObject("a", "Hello"); @@ -43,6 +47,10 @@ public void testDeleteObjectsByKey() { ); } + /** + * Deletes one object using DeleteObjects with a matching ETag condition. + * Expected: Object is deleted; response hasDeleted() is true and deleted list contains "a". + */ @Test public void testDeleteObjectsWithMatchingETag() { bucket.putObject("a", "Hello"); @@ -55,6 +63,10 @@ public void testDeleteObjectsWithMatchingETag() { ); } + /** + * Attempts to delete an object with a non-matching ETag (conditional delete). + * Expected: Object is not deleted; response hasDeleted() is false and deleted list is empty. + */ @Test public void testDeleteObjectsWithDifferentETag() { bucket.putObject("a", "Hello"); @@ -69,6 +81,10 @@ public void testDeleteObjectsWithDifferentETag() { ); } + /** + * Deletes objects whose keys contain ".." (e.g. "a..b", "c..d") via DeleteObjects. + * Expected: Both objects are deleted; response lists "a..b" and "c..d" in deleted. + */ @Test public void testDeleteObjectsContainingDotDot() { bucket.putObject("a..b", "Hello"); diff --git a/src/main/java/com/datadobi/s3test/GetObjectTests.java b/src/main/java/com/datadobi/s3test/GetObjectTests.java index 06d8b9b..9d7e258 100644 --- a/src/main/java/com/datadobi/s3test/GetObjectTests.java +++ b/src/main/java/com/datadobi/s3test/GetObjectTests.java @@ -36,6 +36,10 @@ public class GetObjectTests extends S3TestBase { public GetObjectTests() throws IOException { } + /** + * Puts an object and retrieves it with GET. + * Expected: GetObject returns the full body; content matches original string "Hello, World!". + */ @Test public void testGetObject() throws IOException { var fullContent = "Hello, World!"; @@ -47,6 +51,10 @@ public void testGetObject() throws IOException { } } + /** + * Puts an empty object and retrieves it with GET. + * Expected: GetObject returns 0 bytes; response contentLength is 0. + */ @Test public void testGetEmptyObject() throws IOException { var emptyContent = ""; @@ -59,6 +67,10 @@ public void testGetEmptyObject() throws IOException { } } + /** + * Calls GetObject for a key that does not exist. + * Expected: S3Exception with HTTP status 404 (Not Found). + */ @Test public void testGetKeyThatDoesNotExist() throws IOException { try (var objectInputStream = bucket.getObject("foo")) { @@ -68,6 +80,10 @@ public void testGetKeyThatDoesNotExist() throws IOException { } } + /** + * Gets a byte range (bytes 7 to end) of an object using Range header. + * Expected: Response body is "World!"; Content-Range and Content-Length match the range. + */ @Test public void testGetPartial() throws IOException { var fullContent = "Hello, World!"; @@ -89,6 +105,10 @@ public void testGetPartial() throws IOException { } } + /** + * Requests a byte range that starts beyond the object size (e.g. bytes 200-). + * Expected: S3Exception with HTTP status 416 (Range Not Satisfiable). + */ @Test public void testGetPartialUnsatisfiable() { var fullContent = "Hello, World!"; @@ -102,6 +122,10 @@ public void testGetPartialUnsatisfiable() { } } + /** + * Gets a byte range with both start and end (bytes 0-4). + * Expected: Response body is "Hello"; Content-Range and Content-Length are correct. + */ @Test public void testGetPartialWithEndPosition() throws IOException { var fullContent = "Hello, World!"; @@ -123,6 +147,10 @@ public void testGetPartialWithEndPosition() throws IOException { } } + /** + * Gets a middle byte range (bytes 7-11) of "Hello, World!". + * Expected: Response body is "World"; Content-Range and Content-Length match. + */ @Test public void testGetPartialMiddleRange() throws IOException { var fullContent = "Hello, World!"; @@ -144,6 +172,10 @@ public void testGetPartialMiddleRange() throws IOException { } } + /** + * Gets the last N bytes using suffix-length range (e.g. bytes=-6 for last 6 bytes). + * Expected: Response body is "World!" (last 6 bytes of "Hello, World!"). + */ @Test public void testGetPartialLastBytes() throws IOException { var fullContent = "Hello, World!"; @@ -158,6 +190,10 @@ public void testGetPartialLastBytes() throws IOException { } } + /** + * Requests a range with end position beyond object size (e.g. bytes 10-100). + * Expected: Server returns from start to end of object only; body is "ld!" (bytes 10-12). + */ @Test public void testGetPartialBeyondEnd() throws IOException { var fullContent = "Hello, World!"; diff --git a/src/main/java/com/datadobi/s3test/ListBucketsTests.java b/src/main/java/com/datadobi/s3test/ListBucketsTests.java index 63e46b3..4f13928 100644 --- a/src/main/java/com/datadobi/s3test/ListBucketsTests.java +++ b/src/main/java/com/datadobi/s3test/ListBucketsTests.java @@ -32,6 +32,10 @@ public class ListBucketsTests extends S3TestBase { public ListBucketsTests() throws IOException { } + /** + * Calls ListBuckets and checks the response Date header. + * Expected: Response includes a "Date" header; parsed value is within ~30 seconds of request time (valid RFC 1123). + */ @Test public void listBucketsResponsesShouldReturnValidDateHeader() { var timeOfRequest = Instant.now(); diff --git a/src/main/java/com/datadobi/s3test/ListObjectsTests.java b/src/main/java/com/datadobi/s3test/ListObjectsTests.java index 9e1f552..eec399d 100644 --- a/src/main/java/com/datadobi/s3test/ListObjectsTests.java +++ b/src/main/java/com/datadobi/s3test/ListObjectsTests.java @@ -44,6 +44,10 @@ public class ListObjectsTests extends S3TestBase { public ListObjectsTests() throws IOException { } + /** + * Uploads 143 objects, then lists all keys (implementation uses V2) with page size 7. + * Expected: All 143 keys are returned (paginated); set of keys matches uploaded keys. + */ @Test public void serialListObjectsV1GetsAllKeys() { Set generatedKeys = new HashSet<>(); @@ -57,6 +61,10 @@ public void serialListObjectsV1GetsAllKeys() { assertEquals(generatedKeys, new HashSet<>(keys)); } + /** + * Uploads 143 objects, then lists all keys using ListObjects V2 with page size 7. + * Expected: All 143 keys are returned (paginated); set of keys matches uploaded keys. + */ @Test public void serialListObjectsV2GetsAllKeys() { Set generatedKeys = new HashSet<>(); @@ -70,21 +78,37 @@ public void serialListObjectsV2GetsAllKeys() { assertEquals(generatedKeys, new HashSet<>(keys)); } + /** + * Lists with V2, maxKeys=1, startAfter="180" when keys "180","190","200" exist. + * Expected: One key returned ("190"); isTruncated is true (more keys exist). + */ @Test public void listUntilPenultimateV2ShouldIndicateTruncatedWithExistingStartAfterKey() { listUntilPenultimateShouldIndicateTruncated(V2, "180"); } + /** + * Same as above but startAfter="185" (non-existing key); first key after "185" is "190". + * Expected: One key "190" returned; isTruncated is true. + */ @Test public void listUntilPenultimateV2ShouldIndicateTruncatedWithNonExistingStartAfterKey() { listUntilPenultimateShouldIndicateTruncated(V2, "185"); } + /** + * Lists with V1 (marker), maxKeys=1, startAfter="180"; keys "180","190","200" exist. + * Expected: One key "190"; isTruncated true. + */ @Test public void listUntilPenultimateV1ShouldIndicateTruncatedWithExistingStartAfterKey() { listUntilPenultimateShouldIndicateTruncated(V1, "180"); } + /** + * Same with V1 and startAfter="185" (non-existing). + * Expected: One key "190"; isTruncated true. + */ @Test public void listUntilPenultimateV1ShouldIndicateTruncatedWithNonExistingStartAfterKey() { listUntilPenultimateShouldIndicateTruncated(V1, "185"); @@ -127,6 +151,10 @@ private void listUntilPenultimateShouldIndicateTruncated(S3.ListObjectsVersion v public static final String AFTER_SURROGATES = "\uFB80"; public static final String CP_MAX = "\uDBFF\uDFFF"; + /** + * Puts objects with keys containing BMP and non-BMP (surrogate) codepoints; lists and checks sort order. + * Expected: Keys are returned in UTF-8 binary order (or UTF-16 order if quirk); startAfter filtering works accordingly. + */ @Test public void thatServerSortsInUtf8Binary() { Assume.assumeFalse(target.hasQuirk(KEYS_WITH_CODEPOINTS_OUTSIDE_BMP_REJECTED)); @@ -154,6 +182,10 @@ public void thatServerSortsInUtf8Binary() { } } + /** + * Puts an object, copies it in-place with metadata replace; checks LIST and HEAD ETags. + * Expected: Copy preserves content so ETag unchanged; LIST V1/V2 return same ETag (or "" if quirk ETAG_EMPTY_AFTER_COPY_OBJECT). + */ @Test public void listReturnsSameEtagAsCopyObject() { var putResponse = bucket.putObject("key", "body"); @@ -188,12 +220,20 @@ public void listReturnsSameEtagAsCopyObject() { } } + /** + * Lists objects on an empty bucket (V1 and V2). + * Expected: Both return empty list of keys. + */ @Test public void testListEmpty() { assertEquals(List.of(), bucket.listObjectKeys(V1)); assertEquals(List.of(), bucket.listObjectKeys(V2)); } + /** + * Puts three objects ("a","l","z") and lists with V1 and V2. + * Expected: Both return keys in order ["a","l","z"]. + */ @Test public void testList() { var keys = Arrays.asList("a", "l", "z"); @@ -221,6 +261,10 @@ private void validateListObjects(S3.ListObjectsVersion listObjectsVersion, Map s / 100 == 2, "HTTP 2xx status"); } + /** + * Puts with key containing a null byte (invalid in UTF-8). + * Expected: HTTP 4xx/5xx (rejection); unless KEYS_WITH_NULL_NOT_REJECTED. + */ @Test public void testNullIsRejected() throws IOException { assumeFalse(target.hasQuirk(KEYS_WITH_NULL_NOT_REJECTED)); @@ -176,6 +200,10 @@ public void testNullIsRejected() throws IOException { assertThat(status).as("Put object with invalid UTF-8 bytes should be rejected").matches(s -> s / 100 > 3, "HTTP status of 300 or above"); } + /** + * Puts with key containing overlong null encoding (0xC0 0x80). + * Expected: HTTP 4xx/5xx (invalid UTF-8 rejected). + */ @Test public void testOverlongNullIsRejected() throws IOException { assumeFalse(target.hasQuirk(KEYS_WITH_INVALID_UTF8_NOT_REJECTED)); @@ -191,6 +219,10 @@ public void testOverlongNullIsRejected() throws IOException { assertThat(status).as("Put object with invalid UTF-8 bytes should be rejected").matches(s -> s / 100 > 3, "HTTP status of 300 or above"); } + /** + * Puts with key containing overlong encoding of 'a' (4-byte form). + * Expected: HTTP 4xx/5xx (overlong UTF-8 rejected). + */ @Test public void testOverlongEncodingsAreRejected() throws IOException { assumeFalse(target.hasQuirk(KEYS_WITH_INVALID_UTF8_NOT_REJECTED)); @@ -214,6 +246,10 @@ public void testOverlongEncodingsAreRejected() throws IOException { assertThat(status).as("Put object with invalid UTF-8 bytes should be rejected").matches(s -> s / 100 > 3, "HTTP status of 300 or above"); } + /** + * Puts objects with keys that are Unicode-equivalent under normalization (e.g. Γ… vs A+combining ring). + * Expected: Only the exact key used on put is retrievable; equivalent normalized forms do not match (no server-side normalization). + */ @Test public void testThatServerDoesNotNormalizeCodePoints() throws IOException { // Unicode normalization : https://www.unicode.org/reports/tr15/#Norm_Forms @@ -259,6 +295,10 @@ public void testThatServerDoesNotNormalizeCodePoints() throws IOException { } } + /** + * When null bytes in keys are accepted: puts keys with null and "A"; lists and checks order. + * Expected: Keys sorted in UTF-8 order: "with-null-byte-\0.key" before "with-null-byte-A.key". + */ @Test public void thatServerSortsNullInUtf8Order() throws IOException { assumeTrue(target.hasQuirk(KEYS_WITH_NULL_NOT_REJECTED)); diff --git a/src/main/java/com/datadobi/s3test/PrefixDelimiterTests.java b/src/main/java/com/datadobi/s3test/PrefixDelimiterTests.java index 9952639..27afb54 100644 --- a/src/main/java/com/datadobi/s3test/PrefixDelimiterTests.java +++ b/src/main/java/com/datadobi/s3test/PrefixDelimiterTests.java @@ -37,6 +37,10 @@ public class PrefixDelimiterTests extends S3TestBase { public PrefixDelimiterTests() throws IOException { } + /** + * Lists with prefix="a/", delimiter="/", maxKeys=10. Bucket has a, a/b/0..9, a/c/0..9, a/d. + * Expected: contents = ["a/d"]; commonPrefixes = ["a/b/", "a/c/"]; not truncated. + */ @Test public void testSimple() { bucket.putObject("a", "a"); @@ -64,6 +68,10 @@ public void testSimple() { assertFalse(response.isTruncated()); } + /** + * Lists with prefix="a/b/", delimiter="/". All keys under a/b/ (0..9). + * Expected: contents = all a/b/0..9; commonPrefixes empty; not truncated. + */ @Test public void testTruncatedPrefix() { bucket.putObject("a", "a"); @@ -90,6 +98,10 @@ public void testTruncatedPrefix() { assertFalse(result.isTruncated()); } + /** + * Lists with prefix="a/" and no delimiter; uses continuation tokens across pages. + * Expected: First page a/b/0..9, second a/c/0..9, third a/d; pagination via nextContinuationToken. + */ @Test public void testPrefixOnly() { bucket.putObject("a", "a"); @@ -129,6 +141,10 @@ public void testPrefixOnly() { ); } + /** + * Lists with prefix="a/", delimiter="/", maxKeys=10; many objects and common prefixes. + * Expected: Multiple pages; contents and commonPrefixes match expected batches; truncation/continuation correct. + */ @Test public void testMorePrefixesThanMaxKeys() { for (var i = 0; i < 15; i++) { @@ -178,6 +194,10 @@ public void testMorePrefixesThanMaxKeys() { ); } + /** + * Lists with prefix=null, delimiter="/"; bucket has a, d, b/0..499, c/0..499. + * Expected: First page contents ["a","d"], commonPrefixes ["b/","c/"] (top-level listing). + */ @Test public void testNullPrefix() { bucket.putObject("a", "a"); @@ -205,6 +225,10 @@ public void testNullPrefix() { ); } + /** + * Lists with prefix="/", delimiter="/". No keys start with "/". + * Expected: Empty contents and empty commonPrefixes. + */ @Test public void testSlashPrefix() { bucket.putObject("a", "a"); @@ -230,6 +254,10 @@ public void testSlashPrefix() { ); } + /** + * Lists with prefix="b/", delimiter="/"; bucket has object "b/" and b/0..199, c/0..9, d. + * Expected: First page includes "b/" and b/0, b/1, b/10, etc. (lexicographic order); commonPrefixes empty. + */ @Test public void testPrefixMatchingObjectKey() { bucket.putObject("a", "a"); @@ -258,6 +286,10 @@ public void testPrefixMatchingObjectKey() { ); } + /** + * Single object with key "b/" (trailing slash); list with prefix="", delimiter="/". + * Expected: No contents; commonPrefixes = ["b/"] (key "b/" reported as common prefix). + */ @Test public void testSingleObjectIsReportedAsCommonPrefix() { // Intentionally create an object with a key matching the prefix to see if it's returned or not diff --git a/src/main/java/com/datadobi/s3test/PutObjectTests.java b/src/main/java/com/datadobi/s3test/PutObjectTests.java index b3be3d1..b4b3754 100644 --- a/src/main/java/com/datadobi/s3test/PutObjectTests.java +++ b/src/main/java/com/datadobi/s3test/PutObjectTests.java @@ -37,6 +37,10 @@ public class PutObjectTests extends S3TestBase { public PutObjectTests() throws IOException { } + /** + * Puts an object "foo" with body "bar" and checks HEAD. + * Expected: contentLength 3; HEAD ETag matches put response ETag. + */ @Test public void testPutObject() { var putResponse = bucket.putObject("foo", "bar"); @@ -45,6 +49,10 @@ public void testPutObject() { assertEquals(putResponse.eTag(), headResponse.eTag()); } + /** + * Puts an empty object "foo" (zero-length body). + * Expected: HEAD contentLength 0; ETag matches put response. + */ @Test public void testPutEmptyObject() { var putResponse = bucket.putObject("foo", ""); @@ -53,6 +61,10 @@ public void testPutEmptyObject() { assertEquals(putResponse.eTag(), headResponse.eTag()); } + /** + * Puts an object, then overwrites it with different content. + * Expected: Second put succeeds; GET returns new content and new ETag; ETags differ. + */ @Test public void thatPutObjectCanUpdate() throws Exception { var key = "key"; @@ -85,6 +97,10 @@ public void thatPutObjectCanUpdate() throws Exception { } + /** + * Puts an object with Content-Encoding: gzip and binary gzip body. + * Expected: Object stored as-is; GET returns same bytes and Content-Encoding: gzip. + */ @Test public void thatServerAcceptsContentEncodingGzip() throws Exception { var xml = ""; @@ -109,6 +125,10 @@ public void thatServerAcceptsContentEncodingGzip() throws Exception { } } + /** + * Puts an object with custom Content-Encoding "dd-plain-no-encoding". + * Expected: Object stored; GET returns same content and contentEncoding header. + */ @Test public void thatServerAcceptsArbitraryContentEncoding() throws Exception { var xml = ""; @@ -129,6 +149,10 @@ public void thatServerAcceptsArbitraryContentEncoding() throws Exception { } } + /** + * Puts an empty object with key "content-type/" and Content-Type "text/empty". + * Expected: GET returns Content-Type "text/empty" (unless CONTENT_TYPE_NOT_SET_FOR_KEYS_WITH_TRAILING_SLASH). + */ @Test public void canSetContentTypeOnEmptyObjectWithKeyContainingTrailingSlash() throws IOException { @@ -148,6 +172,10 @@ public void canSetContentTypeOnEmptyObjectWithKeyContainingTrailingSlash() throw } } + /** + * Puts an object, then overwrites with same content but different user metadata. + * Expected: LastModified from HEAD increases after second put. + */ @Test public void thatUpdateMetadataUsingPutObjectAffectsLastModifiedTime() throws Exception { var key = "key"; @@ -172,6 +200,10 @@ public void thatUpdateMetadataUsingPutObjectAffectsLastModifiedTime() throws Exc assertNotEquals("PutObject with different user metadata should change LastModified", info1.lastModified(), info2.lastModified()); } + /** + * Puts an object, then overwrites with different content after a short delay. + * Expected: ETag and LastModified both change after second put. + */ @Test public void thatUpdateDataUsingPutObjectAffectsLastModifiedTime() throws Exception { var key = "key"; diff --git a/src/main/java/com/datadobi/s3test/RunTests.java b/src/main/java/com/datadobi/s3test/RunTests.java index f3fd6ba..bc284a2 100644 --- a/src/main/java/com/datadobi/s3test/RunTests.java +++ b/src/main/java/com/datadobi/s3test/RunTests.java @@ -192,22 +192,38 @@ public void testFinished(Description description) throws Exception { stdOut.println(" πŸ™ˆ"); } else if (failure != null) { stdOut.println(" ❌"); + String shortMessage = getShortFailureMessage(failure); + stdOut.println(" " + shortMessage); if (logPath != null) { - Path logPath = Files.createDirectories( + Path logDir = Files.createDirectories( this.logPath.resolve(description.getTestClass().getSimpleName()) .resolve(description.getMethodName()) ); Files.writeString( - logPath.resolve("error.log"), + logDir.resolve("error.log"), failure.getTrace(), StandardCharsets.UTF_8 ); - } else { - stdOut.println(failure.getTrimmedTrace()); } } else { stdOut.println(" βœ…"); } } + + private static String getShortFailureMessage(Failure failure) { + Throwable ex = failure.getException(); + String name = ex.getClass().getSimpleName(); + String message = ex.getMessage(); + if (message != null && !message.isBlank()) { + // keep first line only, truncate if very long + String firstLine = message.lines().findFirst().orElse("").trim(); + int maxLen = 200; + if (firstLine.length() > maxLen) { + firstLine = firstLine.substring(0, maxLen) + "..."; + } + return name + ": " + firstLine; + } + return name; + } } } From cfc9a7660e923433e440f72c22dc87d2f7697dfe Mon Sep 17 00:00:00 2001 From: "Veer.Kumar" Date: Tue, 10 Feb 2026 21:41:33 +0000 Subject: [PATCH 2/4] Added support to create new bucket if test fails during cleanup and not to fail test if teardown failed --- .../com/datadobi/s3test/s3/S3TestBase.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/datadobi/s3test/s3/S3TestBase.java b/src/main/java/com/datadobi/s3test/s3/S3TestBase.java index 2eb9e41..9f3e775 100644 --- a/src/main/java/com/datadobi/s3test/s3/S3TestBase.java +++ b/src/main/java/com/datadobi/s3test/s3/S3TestBase.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Objects; +import java.util.UUID; public class S3TestBase { public static final Config DEFAULT_CONFIG; @@ -61,6 +62,9 @@ public class S3TestBase { private Description currentTest; + /** When cleanup fails, next test uses this bucket instead of target.bucket(); cleared after use. */ + private static volatile String cleanupFailedNextBucket = null; + @Rule(order = 0) public TestWatcher testName = new TestWatcher() { @Override @@ -92,10 +96,17 @@ public final void setUp() throws IOException { s3 = S3.createClient(target); - this.bucket = new S3Bucket(s3, target.bucket()); + String bucketName = cleanupFailedNextBucket != null ? cleanupFailedNextBucket : target.bucket(); + this.bucket = new S3Bucket(s3, bucketName); if (target.createBucket()) { bucket.create(); } + // If we used a fallback bucket, keep using new buckets for subsequent tests (original may still exist). + if (cleanupFailedNextBucket != null) { + cleanupFailedNextBucket = "s3test-" + UUID.randomUUID(); + } else { + cleanupFailedNextBucket = null; + } if (!CAPTURE_SETUP) { WIRE_LOGGER.start(currentTest); @@ -108,9 +119,15 @@ public final void tearDown() { WIRE_LOGGER.stop(); } - S3.clearBucket(s3, target.bucket()); - if (target.createBucket()) { - bucket.delete(); + try { + S3.clearBucket(s3, bucket.name()); + if (target.createBucket()) { + bucket.delete(); + } + } catch (Throwable t) { + // Cleanup failed (e.g. bucket not empty): use a new bucket for next test and do not + // fail this test β€” only the test method's result counts. + cleanupFailedNextBucket = "s3test-" + UUID.randomUUID(); } s3.close(); From 649c0d93bb74cc5f5e5611a47b33dbe9ad8963ca Mon Sep 17 00:00:00 2001 From: "Veer.Kumar" Date: Tue, 10 Feb 2026 22:25:20 +0000 Subject: [PATCH 3/4] Fixed thatConditionalPutIfMatchEtagWorks --- AWS_VS_CUSTOM_COMPARISON.md | 61 ++++++++++++ FAILED_TESTS.md | 92 +++++++++++++++++++ .../s3test/ConditionalRequestTests.java | 1 - .../java/com/datadobi/s3test/s3/Config.java | 5 +- 4 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 AWS_VS_CUSTOM_COMPARISON.md create mode 100644 FAILED_TESTS.md diff --git a/AWS_VS_CUSTOM_COMPARISON.md b/AWS_VS_CUSTOM_COMPARISON.md new file mode 100644 index 0000000..9ad017a --- /dev/null +++ b/AWS_VS_CUSTOM_COMPARISON.md @@ -0,0 +1,61 @@ +# S3 Test Results: AWS S3 vs Custom Endpoint (10.42.236.12) + +Comparison of test results run with **no quirks** (conditional PUT quirks removed). + +| Test | Custom (10.42.236.12) | AWS S3 | +|------|------------------------|--------| +| **ConditionalRequestTests** | | | +| thatConditionalPutIfMatchEtagWorks | βœ… (after fix) | βœ… | +| thatConditionalPutIfNoneMatchEtagWorks | ❌ expected 412, got 501 | ❌ expected 412, got 501 | +| thatConditionalPutIfNoneMatchStarWorks | βœ… | βœ… | +| **DeleteObjectsTests** | | | +| testDeleteObjectsWithDifferentETag | ❌ (server deleted despite wrong ETag) | βœ… (server respected ETag) | +| testDeleteObjectsWithMatchingETag | βœ… | βœ… | +| testDeleteObjectsContainingDotDot | βœ… | βœ… | +| testDeleteObjectsByKey | βœ… | βœ… | +| **MultiPartUploadTests** | | | +| thatMultipartRetrievesOriginalParts | ❌ (part count / HEAD part not supported) | βœ… | +| **ObjectKeyTests** | | | +| testSurrogatePairsAreRejected | ❌ (server accepted invalid UTF-8) | βœ… (server rejected) | +| testOverlongEncodingsAreRejected | ❌ (server accepted) | βœ… (server rejected) | +| testOverlongNullIsRejected | ❌ (server accepted) | βœ… (server rejected) | +| testCodePointMinIsAccepted | βœ… | βœ… | +| testKeyNamesWithHighCodePointsAreAccepted | βœ… | βœ… | +| testNullIsRejected | βœ… | βœ… | +| testSimpleObjectKeySigning | βœ… | βœ… | +| testThatServerDoesNotNormalizeCodePoints | βœ… | βœ… | +| **Others (Checksum, DeleteObject, GetObject, ListBuckets, ListObjects, PrefixDelimiter)** | βœ… | βœ… (through testNullPrefix; run timed out before PutObjectTests) | + +--- + +## Summary of differences + +1. **thatConditionalPutIfNoneMatchEtagWorks** + **Same on both:** AWS and your endpoint return **501** for If-None-Match with an ETag. The test expects **412**. So neither matches the test’s expectation; this is a test/expectation issue or an optional feature. + +2. **testDeleteObjectsWithDifferentETag** + **Custom:** Deletes the object even when the request ETag is wrong (conditional delete not enforced). + **AWS:** Does not delete when ETag does not match (conditional delete enforced). + **Gap:** Custom endpoint does not enforce optional ETag on DeleteObjects. + +3. **thatMultipartRetrievesOriginalParts** + **Custom:** Fails (HEAD with part number or part count not supported or not as expected). + **AWS:** Passes (full multipart and part metadata support). + **Gap:** Custom endpoint is missing or differing multipart metadata (e.g. `partsCount`, HEAD by part number). + +4. **testSurrogatePairsAreRejected / testOverlongEncodingsAreRejected / testOverlongNullIsRejected** + **Custom:** Accepts invalid UTF-8 keys (returns 2xx). + **AWS:** Rejects them (4xx/5xx). + **Gap:** Custom endpoint does not validate object keys for invalid UTF-8 (surrogates, overlong encodings). + +--- + +## Conclusion + +- **Conditional PUT (If-Match):** Both support it; the test fix made the behavior consistent with expectations. +- **Conditional PUT (If-None-Match with ETag):** Both return 501; test expects 412. +- **DeleteObjects with ETag:** Only AWS enforces the condition; custom endpoint should be updated to honor ETag when provided. +- **Multipart metadata:** Only AWS passes; custom endpoint needs HEAD/GET part number and correct part count behavior. +- **UTF-8 key validation:** Only AWS rejects invalid UTF-8 keys; custom endpoint should reject surrogate and overlong encodings for S3 compatibility. + +*Note: The AWS run was stopped by a timeout during PrefixDelimiterTests; all tests up to that point completed. PutObjectTests were not included in the AWS run due to the timeout.* diff --git a/FAILED_TESTS.md b/FAILED_TESTS.md new file mode 100644 index 0000000..7af3ed1 --- /dev/null +++ b/FAILED_TESTS.md @@ -0,0 +1,92 @@ +# Description of Failed Tests and Why They Failed + +This document describes each test that failed when running the S3 test suite against the endpoint (with no quirks configured) and the reason for each failure. + +--- + +## 1. **thatConditionalPutIfMatchEtagWorks** (ConditionalRequestTests) + +**What the test does:** +Puts an object `"object"` with content `"hello"`, then overwrites it with content `"bar"` using **If-Match: <current-etag>** (the ETag from the first PUT). The precondition is satisfied, so the overwrite is expected to succeed and the object should become `"bar"`. + +**Why it failed:** +The test has a **bug in the success path**. When the overwrite succeeds (no exception), it asserts that the content is `"bar"` and that the ETags match, then it **always** calls `fail("PutObject using 'If-Match: \"\"' should fail if object etag does not match")`. So any successful overwrite is reported as a failure. The server behaved correctly (it allowed the overwrite when If-Match matched); the failure is due to this erroneous `fail()` in the test code. + +--- + +## 2. **thatConditionalPutIfNoneMatchEtagWorks** (ConditionalRequestTests) + +**What the test does:** +Puts an object, then attempts to overwrite it with **If-None-Match: <current-etag>** (meaning β€œonly write if the current ETag is different”). Since the object already has that ETag, the overwrite should be rejected with **412 Precondition Failed**. + +**Why it failed:** +The test expects HTTP **412** (Precondition Failed). The server returned **501** (Not Implemented). So the endpoint does not support the **If-None-Match** conditional with an ETag value and reports the feature as not implemented instead of returning 412. + +--- + +## 3. **testDeleteObjectsWithDifferentETag** (DeleteObjectsTests) + +**What the test does:** +Creates an object `"a"` with content `"Hello"`, then calls **DeleteObjects** for key `"a"` with a **wrong ETag** (`"foo"`). The test expects that the object is **not** deleted (conditional delete): `hasDeleted()` should be false and the deleted list should be empty. + +**Why it failed:** +The assertion failed: either `hasDeleted()` was true or the deleted list was non-empty. So the server **deleted the object even though the ETag did not match**. The S3 endpoint does not enforce the optional ETag condition on DeleteObjects; it deletes the object regardless of the supplied ETag. + +--- + +## 4. **thatMultipartRetrievesOriginalParts** (MultiPartUploadTests) + +**What the test does:** +Performs a **multipart upload** with 11 parts (sizes from 3 MB to 13 MB, each β‰₯ 5 MB where required), completes the upload, then uses **HEAD Object** with `partNumber(1)` to get metadata. It checks that `partsCount()` equals 11 (or that the quirk `MULTIPART_SIZES_NOT_KEPT` or `GET_OBJECT_PARTCOUNT_NOT_SUPPORTED` applies), then either fetches by part number or as a single object and verifies total size matches. + +**Why it failed:** +The failure is an **AssertionError** at the multipart-metadata checks (around lines 97–104). Most likely: + +- **HEAD Object** with `partNumber(1)` either is not supported (e.g. 4xx/5xx), or +- **partsCount()** is null or not 11. + +So the server either does not support the part-number parameter on HEAD/GET, or does not report part count. The test then cannot validate part count or per-part sizes and fails the corresponding assertions. + +--- + +## 5. **testSurrogatePairsAreRejected** (ObjectKeyTests) + +**What the test does:** +Sends a **PUT** with an object key that contains **invalid UTF-8**: the key includes the raw UTF-8 encoding of surrogate code units (high and low surrogates) instead of the single UTF-8 sequence for the corresponding supplementary character. Per Unicode/UTF-8, such surrogate-byte sequences in UTF-8 are invalid. The test expects the server to **reject** the request with an HTTP status in the **4xx or 5xx** range. + +**Why it failed:** +The assertion `status / 100 > 3` failed, so the server returned a **2xx** status and **accepted** the key. The endpoint does not validate UTF-8 key encoding and allows keys that contain invalid UTF-8 (surrogate pairs encoded as separate surrogate code units in the byte stream). + +--- + +## 6. **testOverlongEncodingsAreRejected** (ObjectKeyTests) + +**What the test does:** +Sends a **PUT** with an object key that contains an **overlong UTF-8 encoding**: the character `'a'` encoded in 4 bytes instead of 1. Overlong encodings are invalid in UTF-8. The test expects the server to **reject** the request with **4xx or 5xx**. + +**Why it failed:** +The server returned **2xx** and accepted the key. The endpoint does not reject overlong UTF-8 sequences in object keys. + +--- + +## 7. **testOverlongNullIsRejected** (ObjectKeyTests) + +**What the test does:** +Sends a **PUT** with an object key that contains the **overlong encoding of the null character** (bytes `0xC0 0x80` instead of `0x00`). This is invalid UTF-8. The test expects **4xx or 5xx**. + +**Why it failed:** +The server returned **2xx** and accepted the key. The endpoint does not reject this invalid UTF-8 encoding in keys. + +--- + +## Summary Table + +| Test | Cause of failure | +|------|------------------| +| **thatConditionalPutIfMatchEtagWorks** | Test bug: `fail()` called on success path; server behavior is correct. | +| **thatConditionalPutIfNoneMatchEtagWorks** | Server returns 501 (Not Implemented) instead of 412 for If-None-Match with ETag. | +| **testDeleteObjectsWithDifferentETag** | Server ignores ETag on DeleteObjects and deletes the object anyway. | +| **thatMultipartRetrievesOriginalParts** | HEAD/GET with part number or parts count not supported or not as expected. | +| **testSurrogatePairsAreRejected** | Server accepts invalid UTF-8 (surrogate bytes) in keys (2xx instead of 4xx/5xx). | +| **testOverlongEncodingsAreRejected** | Server accepts overlong UTF-8 encoding in keys (2xx instead of 4xx/5xx). | +| **testOverlongNullIsRejected** | Server accepts overlong null encoding in keys (2xx instead of 4xx/5xx). | diff --git a/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java b/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java index 11fc1d4..5eee543 100644 --- a/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java +++ b/src/main/java/com/datadobi/s3test/ConditionalRequestTests.java @@ -137,7 +137,6 @@ public void thatConditionalPutIfMatchEtagWorks() throws IOException, Interrupted if (!target.hasQuirk(PUT_OBJECT_IF_MATCH_ETAG_NOT_SUPPORTED)) { assertEquals("bar", content); assertEquals(overwritePutResponse.eTag(), getResponse.eTag()); - fail("PutObject using 'If-Match: \"\"' should fail if object etag does not match"); } } catch (S3Exception e) { if (!target.hasQuirk(PUT_OBJECT_IF_MATCH_ETAG_NOT_SUPPORTED)) { diff --git a/src/main/java/com/datadobi/s3test/s3/Config.java b/src/main/java/com/datadobi/s3test/s3/Config.java index d7bcdb5..55d748c 100644 --- a/src/main/java/com/datadobi/s3test/s3/Config.java +++ b/src/main/java/com/datadobi/s3test/s3/Config.java @@ -9,10 +9,7 @@ import java.util.Locale; public record Config(ImmutableSet quirks) { - public static final Config AWS_CONFIG = new Config(ImmutableSet.of( - Quirk.PUT_OBJECT_IF_NONE_MATCH_ETAG_NOT_SUPPORTED, - Quirk.PUT_OBJECT_IF_MATCH_ETAG_NOT_SUPPORTED - )); + public static final Config AWS_CONFIG = new Config(ImmutableSet.of()); public static Config loadFromToml(Path path) { var quirks = EnumSet.noneOf(Quirk.class); From 1fb3dd4212dadbc1ba72e7fb5a555cd956d3a6ca Mon Sep 17 00:00:00 2001 From: "Veer.Kumar" Date: Tue, 10 Feb 2026 22:29:59 +0000 Subject: [PATCH 4/4] Fixed thatConditionalPutIfMatchEtagWorks --- AWS_VS_CUSTOM_COMPARISON.md | 61 ------------------------ FAILED_TESTS.md | 92 ------------------------------------- 2 files changed, 153 deletions(-) delete mode 100644 AWS_VS_CUSTOM_COMPARISON.md delete mode 100644 FAILED_TESTS.md diff --git a/AWS_VS_CUSTOM_COMPARISON.md b/AWS_VS_CUSTOM_COMPARISON.md deleted file mode 100644 index 9ad017a..0000000 --- a/AWS_VS_CUSTOM_COMPARISON.md +++ /dev/null @@ -1,61 +0,0 @@ -# S3 Test Results: AWS S3 vs Custom Endpoint (10.42.236.12) - -Comparison of test results run with **no quirks** (conditional PUT quirks removed). - -| Test | Custom (10.42.236.12) | AWS S3 | -|------|------------------------|--------| -| **ConditionalRequestTests** | | | -| thatConditionalPutIfMatchEtagWorks | βœ… (after fix) | βœ… | -| thatConditionalPutIfNoneMatchEtagWorks | ❌ expected 412, got 501 | ❌ expected 412, got 501 | -| thatConditionalPutIfNoneMatchStarWorks | βœ… | βœ… | -| **DeleteObjectsTests** | | | -| testDeleteObjectsWithDifferentETag | ❌ (server deleted despite wrong ETag) | βœ… (server respected ETag) | -| testDeleteObjectsWithMatchingETag | βœ… | βœ… | -| testDeleteObjectsContainingDotDot | βœ… | βœ… | -| testDeleteObjectsByKey | βœ… | βœ… | -| **MultiPartUploadTests** | | | -| thatMultipartRetrievesOriginalParts | ❌ (part count / HEAD part not supported) | βœ… | -| **ObjectKeyTests** | | | -| testSurrogatePairsAreRejected | ❌ (server accepted invalid UTF-8) | βœ… (server rejected) | -| testOverlongEncodingsAreRejected | ❌ (server accepted) | βœ… (server rejected) | -| testOverlongNullIsRejected | ❌ (server accepted) | βœ… (server rejected) | -| testCodePointMinIsAccepted | βœ… | βœ… | -| testKeyNamesWithHighCodePointsAreAccepted | βœ… | βœ… | -| testNullIsRejected | βœ… | βœ… | -| testSimpleObjectKeySigning | βœ… | βœ… | -| testThatServerDoesNotNormalizeCodePoints | βœ… | βœ… | -| **Others (Checksum, DeleteObject, GetObject, ListBuckets, ListObjects, PrefixDelimiter)** | βœ… | βœ… (through testNullPrefix; run timed out before PutObjectTests) | - ---- - -## Summary of differences - -1. **thatConditionalPutIfNoneMatchEtagWorks** - **Same on both:** AWS and your endpoint return **501** for If-None-Match with an ETag. The test expects **412**. So neither matches the test’s expectation; this is a test/expectation issue or an optional feature. - -2. **testDeleteObjectsWithDifferentETag** - **Custom:** Deletes the object even when the request ETag is wrong (conditional delete not enforced). - **AWS:** Does not delete when ETag does not match (conditional delete enforced). - **Gap:** Custom endpoint does not enforce optional ETag on DeleteObjects. - -3. **thatMultipartRetrievesOriginalParts** - **Custom:** Fails (HEAD with part number or part count not supported or not as expected). - **AWS:** Passes (full multipart and part metadata support). - **Gap:** Custom endpoint is missing or differing multipart metadata (e.g. `partsCount`, HEAD by part number). - -4. **testSurrogatePairsAreRejected / testOverlongEncodingsAreRejected / testOverlongNullIsRejected** - **Custom:** Accepts invalid UTF-8 keys (returns 2xx). - **AWS:** Rejects them (4xx/5xx). - **Gap:** Custom endpoint does not validate object keys for invalid UTF-8 (surrogates, overlong encodings). - ---- - -## Conclusion - -- **Conditional PUT (If-Match):** Both support it; the test fix made the behavior consistent with expectations. -- **Conditional PUT (If-None-Match with ETag):** Both return 501; test expects 412. -- **DeleteObjects with ETag:** Only AWS enforces the condition; custom endpoint should be updated to honor ETag when provided. -- **Multipart metadata:** Only AWS passes; custom endpoint needs HEAD/GET part number and correct part count behavior. -- **UTF-8 key validation:** Only AWS rejects invalid UTF-8 keys; custom endpoint should reject surrogate and overlong encodings for S3 compatibility. - -*Note: The AWS run was stopped by a timeout during PrefixDelimiterTests; all tests up to that point completed. PutObjectTests were not included in the AWS run due to the timeout.* diff --git a/FAILED_TESTS.md b/FAILED_TESTS.md deleted file mode 100644 index 7af3ed1..0000000 --- a/FAILED_TESTS.md +++ /dev/null @@ -1,92 +0,0 @@ -# Description of Failed Tests and Why They Failed - -This document describes each test that failed when running the S3 test suite against the endpoint (with no quirks configured) and the reason for each failure. - ---- - -## 1. **thatConditionalPutIfMatchEtagWorks** (ConditionalRequestTests) - -**What the test does:** -Puts an object `"object"` with content `"hello"`, then overwrites it with content `"bar"` using **If-Match: <current-etag>** (the ETag from the first PUT). The precondition is satisfied, so the overwrite is expected to succeed and the object should become `"bar"`. - -**Why it failed:** -The test has a **bug in the success path**. When the overwrite succeeds (no exception), it asserts that the content is `"bar"` and that the ETags match, then it **always** calls `fail("PutObject using 'If-Match: \"\"' should fail if object etag does not match")`. So any successful overwrite is reported as a failure. The server behaved correctly (it allowed the overwrite when If-Match matched); the failure is due to this erroneous `fail()` in the test code. - ---- - -## 2. **thatConditionalPutIfNoneMatchEtagWorks** (ConditionalRequestTests) - -**What the test does:** -Puts an object, then attempts to overwrite it with **If-None-Match: <current-etag>** (meaning β€œonly write if the current ETag is different”). Since the object already has that ETag, the overwrite should be rejected with **412 Precondition Failed**. - -**Why it failed:** -The test expects HTTP **412** (Precondition Failed). The server returned **501** (Not Implemented). So the endpoint does not support the **If-None-Match** conditional with an ETag value and reports the feature as not implemented instead of returning 412. - ---- - -## 3. **testDeleteObjectsWithDifferentETag** (DeleteObjectsTests) - -**What the test does:** -Creates an object `"a"` with content `"Hello"`, then calls **DeleteObjects** for key `"a"` with a **wrong ETag** (`"foo"`). The test expects that the object is **not** deleted (conditional delete): `hasDeleted()` should be false and the deleted list should be empty. - -**Why it failed:** -The assertion failed: either `hasDeleted()` was true or the deleted list was non-empty. So the server **deleted the object even though the ETag did not match**. The S3 endpoint does not enforce the optional ETag condition on DeleteObjects; it deletes the object regardless of the supplied ETag. - ---- - -## 4. **thatMultipartRetrievesOriginalParts** (MultiPartUploadTests) - -**What the test does:** -Performs a **multipart upload** with 11 parts (sizes from 3 MB to 13 MB, each β‰₯ 5 MB where required), completes the upload, then uses **HEAD Object** with `partNumber(1)` to get metadata. It checks that `partsCount()` equals 11 (or that the quirk `MULTIPART_SIZES_NOT_KEPT` or `GET_OBJECT_PARTCOUNT_NOT_SUPPORTED` applies), then either fetches by part number or as a single object and verifies total size matches. - -**Why it failed:** -The failure is an **AssertionError** at the multipart-metadata checks (around lines 97–104). Most likely: - -- **HEAD Object** with `partNumber(1)` either is not supported (e.g. 4xx/5xx), or -- **partsCount()** is null or not 11. - -So the server either does not support the part-number parameter on HEAD/GET, or does not report part count. The test then cannot validate part count or per-part sizes and fails the corresponding assertions. - ---- - -## 5. **testSurrogatePairsAreRejected** (ObjectKeyTests) - -**What the test does:** -Sends a **PUT** with an object key that contains **invalid UTF-8**: the key includes the raw UTF-8 encoding of surrogate code units (high and low surrogates) instead of the single UTF-8 sequence for the corresponding supplementary character. Per Unicode/UTF-8, such surrogate-byte sequences in UTF-8 are invalid. The test expects the server to **reject** the request with an HTTP status in the **4xx or 5xx** range. - -**Why it failed:** -The assertion `status / 100 > 3` failed, so the server returned a **2xx** status and **accepted** the key. The endpoint does not validate UTF-8 key encoding and allows keys that contain invalid UTF-8 (surrogate pairs encoded as separate surrogate code units in the byte stream). - ---- - -## 6. **testOverlongEncodingsAreRejected** (ObjectKeyTests) - -**What the test does:** -Sends a **PUT** with an object key that contains an **overlong UTF-8 encoding**: the character `'a'` encoded in 4 bytes instead of 1. Overlong encodings are invalid in UTF-8. The test expects the server to **reject** the request with **4xx or 5xx**. - -**Why it failed:** -The server returned **2xx** and accepted the key. The endpoint does not reject overlong UTF-8 sequences in object keys. - ---- - -## 7. **testOverlongNullIsRejected** (ObjectKeyTests) - -**What the test does:** -Sends a **PUT** with an object key that contains the **overlong encoding of the null character** (bytes `0xC0 0x80` instead of `0x00`). This is invalid UTF-8. The test expects **4xx or 5xx**. - -**Why it failed:** -The server returned **2xx** and accepted the key. The endpoint does not reject this invalid UTF-8 encoding in keys. - ---- - -## Summary Table - -| Test | Cause of failure | -|------|------------------| -| **thatConditionalPutIfMatchEtagWorks** | Test bug: `fail()` called on success path; server behavior is correct. | -| **thatConditionalPutIfNoneMatchEtagWorks** | Server returns 501 (Not Implemented) instead of 412 for If-None-Match with ETag. | -| **testDeleteObjectsWithDifferentETag** | Server ignores ETag on DeleteObjects and deletes the object anyway. | -| **thatMultipartRetrievesOriginalParts** | HEAD/GET with part number or parts count not supported or not as expected. | -| **testSurrogatePairsAreRejected** | Server accepts invalid UTF-8 (surrogate bytes) in keys (2xx instead of 4xx/5xx). | -| **testOverlongEncodingsAreRejected** | Server accepts overlong UTF-8 encoding in keys (2xx instead of 4xx/5xx). | -| **testOverlongNullIsRejected** | Server accepts overlong null encoding in keys (2xx instead of 4xx/5xx). |