@@ -25,13 +25,15 @@ class S3FeaturesTest {
2525 private static final String BUCKET_VERSIONS = "compat-versions-bucket" ;
2626 private static final String BUCKET_PAB = "compat-pab-bucket" ;
2727 private static final String BUCKET_PAGINATE = "compat-paginate-bucket" ;
28+ private static final String BUCKET_334 = "compat-334-bucket" ;
2829
2930 @ BeforeAll
3031 static void setup () {
3132 s3 = TestFixtures .s3Client ();
3233 createBucket (BUCKET_VERSIONS );
3334 createBucket (BUCKET_PAB );
3435 createBucket (BUCKET_PAGINATE );
36+ createBucket (BUCKET_334 );
3537 }
3638
3739 @ AfterAll
@@ -40,9 +42,11 @@ static void cleanup() {
4042 deleteBucketContents (BUCKET_VERSIONS );
4143 deleteBucketContents (BUCKET_PAB );
4244 deleteBucketContents (BUCKET_PAGINATE );
45+ deleteBucketContents (BUCKET_334 );
4346 quietDeleteBucket (BUCKET_VERSIONS );
4447 quietDeleteBucket (BUCKET_PAB );
4548 quietDeleteBucket (BUCKET_PAGINATE );
49+ quietDeleteBucket (BUCKET_334 );
4650 s3 .close ();
4751 }
4852
@@ -129,6 +133,69 @@ void listObjectVersionsPaginatorPaginates() {
129133 assertThat (allVersions .size ()).isGreaterThanOrEqualTo (5 );
130134 }
131135
136+ // ─────────────────────────────────────────────────────────────────────────
137+ // Issue #334 — listObjectVersions must return non-versioned objects
138+ // ─────────────────────────────────────────────────────────────────────────
139+
140+ /**
141+ * Objects uploaded to a bucket that has never had versioning enabled must appear in
142+ * ListObjectVersions with VersionId="null" (the literal string, per AWS spec).
143+ */
144+ @ Test
145+ @ Order (13 )
146+ @ DisplayName ("#334 listObjectVersions: non-versioned bucket returns objects with VersionId=null" )
147+ void listObjectVersionsNonVersionedBucketReturnsObjects () {
148+ s3 .putObject (PutObjectRequest .builder ().bucket (BUCKET_334 ).key ("file-a.txt" ).build (),
149+ RequestBody .fromString ("content-a" ));
150+ s3 .putObject (PutObjectRequest .builder ().bucket (BUCKET_334 ).key ("file-b.txt" ).build (),
151+ RequestBody .fromString ("content-b" ));
152+
153+ ListObjectVersionsResponse response = s3 .listObjectVersions (
154+ ListObjectVersionsRequest .builder ().bucket (BUCKET_334 ).build ());
155+
156+ List <ObjectVersion > versions = response .versions ();
157+ assertThat (versions ).hasSize (2 );
158+
159+ List <String > keys = versions .stream ().map (ObjectVersion ::key ).toList ();
160+ assertThat (keys ).containsExactlyInAnyOrder ("file-a.txt" , "file-b.txt" );
161+
162+ // AWS returns the literal string "null" for objects uploaded without versioning
163+ assertThat (versions ).allMatch (v -> "null" .equals (v .versionId ()));
164+ assertThat (versions ).allMatch (ObjectVersion ::isLatest );
165+ }
166+
167+ /**
168+ * Objects uploaded before versioning was enabled must appear in ListObjectVersions
169+ * alongside objects uploaded after versioning was enabled.
170+ * Pre-versioning objects appear with VersionId="null"; post-versioning objects have a UUID.
171+ */
172+ @ Test
173+ @ Order (14 )
174+ @ DisplayName ("#334 listObjectVersions: pre-versioning objects appear alongside versioned entries" )
175+ void listObjectVersionsPreVersioningObjectsAppearsWithNullVersionId () {
176+ // plain.txt was put at order 10, before versioning was enabled at order 11.
177+ // It should appear in the listing with VersionId="null".
178+ ListObjectVersionsResponse response = s3 .listObjectVersions (
179+ ListObjectVersionsRequest .builder ().bucket (BUCKET_VERSIONS ).build ());
180+
181+ List <ObjectVersion > all = response .versions ();
182+
183+ // Pre-versioning object
184+ List <ObjectVersion > plainVersions = all .stream ()
185+ .filter (v -> "plain.txt" .equals (v .key ()))
186+ .toList ();
187+ assertThat (plainVersions ).hasSize (1 );
188+ assertThat (plainVersions .get (0 ).versionId ()).isEqualTo ("null" );
189+ assertThat (plainVersions .get (0 ).isLatest ()).isTrue ();
190+
191+ // Versioned objects uploaded after versioning was enabled must have UUID version IDs
192+ List <ObjectVersion > versioned = all .stream ()
193+ .filter (v -> "versioned.txt" .equals (v .key ()))
194+ .toList ();
195+ assertThat (versioned ).hasSizeGreaterThanOrEqualTo (2 );
196+ assertThat (versioned ).allMatch (v -> v .versionId () != null && !"null" .equals (v .versionId ()));
197+ }
198+
132199 // ─────────────────────────────────────────────────────────────────────────
133200 // Issue #236 — PutPublicAccessBlock must not return BucketAlreadyOwnedByYou
134201 // ─────────────────────────────────────────────────────────────────────────
0 commit comments