Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Version 5.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav
## 5.3.0 - PLANNED

* Features and fixes
* TBD
* fix: `GetObjectTagging` now always returns a `Tagging` document with an empty `TagSet` when the object has no tags, instead of a `200` with no body. ([#3149](https://github.com/adobe/S3Mock/issues/3149))
* Version updates (deliverable dependencies)
* TBD
* Version updates (build dependencies)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2025 Adobe.
* Copyright 2017-2026 Adobe.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,9 +22,14 @@ import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.Tag
import software.amazon.awssdk.services.s3.model.Tagging
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

internal class ObjectTaggingIT : S3TestBase() {
private val s3Client: S3Client = createS3Client()
private val httpClient: HttpClient = createHttpClient()

@Test
@S3VerifiedSuccess(year = 2025)
Expand All @@ -48,6 +53,57 @@ internal class ObjectTaggingIT : S3TestBase() {
).isEmpty()
}

@Test
@S3VerifiedFailure(
year = 2026,
reason = "No credentials sent in plain HTTP request",
)
fun `GET ObjectTagging returns an empty TagSet element with no tags`(testInfo: TestInfo) {
val key = UPLOAD_FILE_NAME
val bucketName = givenBucket(testInfo)
s3Client.putObject(
{
it.bucket(bucketName)
it.key(key)
},
RequestBody.fromString("foo"),
)

// The AWS SDK tolerates an empty body, so it cannot tell "no tags" from "no
// document". Read the raw response to pin down the wire format clients parse.
val body = getObjectTaggingBody(bucketName, key)

assertThat(body).contains("<Tagging")
assertThat(body).containsPattern("<TagSet\\s*/>|<TagSet\\s*>\\s*</TagSet>")
}

@Test
@S3VerifiedFailure(
year = 2026,
reason = "No credentials sent in plain HTTP request",
)
fun `GET ObjectTagging returns an empty TagSet element after DELETE`(testInfo: TestInfo) {
val key = UPLOAD_FILE_NAME
val (bucketName, _) = givenBucketAndObject(testInfo, key)

s3Client.putObjectTagging {
it.bucket(bucketName)
it.key(key)
it.tagging {
it.tagSet(tag("tag1" to "foo"))
}
}
s3Client.deleteObjectTagging {
it.bucket(bucketName)
it.key(key)
}

val body = getObjectTaggingBody(bucketName, key)

assertThat(body).contains("<Tagging")
assertThat(body).containsPattern("<TagSet\\s*/>|<TagSet\\s*>\\s*</TagSet>")
}

@Test
@S3VerifiedSuccess(year = 2025)
fun `PUT and GET ObjectTagging succeeds`(testInfo: TestInfo) {
Expand Down Expand Up @@ -170,6 +226,20 @@ internal class ObjectTaggingIT : S3TestBase() {
)
}

private fun getObjectTaggingBody(
bucketName: String,
key: String,
): String {
val request =
HttpRequest
.newBuilder(URI.create("$serviceEndpoint/$bucketName/$key?tagging"))
.GET()
.build()
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
assertThat(response.statusCode()).isEqualTo(200)
return response.body()
}

private fun tag(
key: String,
value: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ class ObjectTaggingController(
val bucket = bucketService.verifyBucketExists(bucketName)
val s3ObjectMetadata = objectService.verifyObjectExists(bucketName, key.key, versionId)

val tagging =
s3ObjectMetadata.tags
?.takeIf { it.isNotEmpty() }
?.let { Tagging(TagSet(it)) }
// S3 always answers with a Tagging document, carrying an empty TagSet when
// the object has no tags. Returning no body at all makes clients that expect
// the documented XML fail to parse the response.
val tagging = Tagging(TagSet(s3ObjectMetadata.tags.orEmpty()))

Comment thread
avikivity marked this conversation as resolved.
return ResponseEntity
.ok()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,39 @@ internal class ObjectTaggingControllerTest : BaseControllerTest() {
.andExpect(content().string(MAPPER.writeValueAsString(tagging)))
}

@Test
fun testGetObjectTagging_NoTags_ReturnsEmptyTagSet() {
givenBucket()
val key = "name"
// No tags were ever set on the object: S3 answers with a Tagging document
// carrying an empty TagSet, not with an empty body.
val s3ObjectMetadata = s3ObjectMetadata(key, UUID.randomUUID().toString())
whenever(objectService.verifyObjectExists("test-bucket", key, null))
.thenReturn(s3ObjectMetadata)

val uri =
UriComponentsBuilder
.fromUriString("/test-bucket/$key")
.queryParam(AwsHttpParameters.TAGGING, "ignored")
.build()
.toString()
mockMvc
.perform(
get(uri)
.accept(MediaType.APPLICATION_XML)
.contentType(MediaType.APPLICATION_XML),
).andExpect(status().isOk)
// Assert the literal wire format instead of round-tripping through the same
// mapper the controller uses: the point of this test is that <TagSet> is
// present in the response body even when there is nothing to report.
.andExpect(
content().string(
"""<?xml version="1.0" encoding="UTF-8"?>""" +
"""<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><TagSet/></Tagging>""",
),
)
}

@Test
fun testPutObjectTagging_Ok() {
givenBucket()
Expand Down
Loading