Skip to content

Commit 78c00ad

Browse files
committed
HeadObject conditional request ITs
Fixes #2371
1 parent 8d3618b commit 78c00ad

File tree

1 file changed

+196
-23
lines changed

1 file changed

+196
-23
lines changed

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

Lines changed: 196 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.adobe.testing.s3mock.its
1919
import com.adobe.testing.s3mock.util.DigestUtil
2020
import org.assertj.core.api.Assertions.assertThat
2121
import org.assertj.core.api.Assertions.assertThatThrownBy
22+
import org.junit.jupiter.api.Disabled
2223
import org.junit.jupiter.api.Test
2324
import org.junit.jupiter.api.TestInfo
2425
import org.junit.jupiter.params.ParameterizedTest
@@ -901,7 +902,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
901902

902903
@Test
903904
@S3VerifiedSuccess(year = 2025)
904-
fun `PUT object succeeds with matching etag`(testInfo: TestInfo) {
905+
fun `PUT object succeeds with if-match=true`(testInfo: TestInfo) {
905906
val uploadFile = File(UPLOAD_FILE_NAME)
906907
val matchingEtag = FileInputStream(uploadFile).let {
907908
"\"${DigestUtil.hexDigest(it)}\""
@@ -922,7 +923,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
922923

923924
@Test
924925
@S3VerifiedSuccess(year = 2025)
925-
fun `PUT object fails with non matching wildcard etag`(testInfo: TestInfo) {
926+
fun `PUT object fails with if-none-match=false with wildcard`(testInfo: TestInfo) {
926927
val uploadFile = File(UPLOAD_FILE_NAME)
927928
val expectedEtag = FileInputStream(uploadFile).let {
928929
"\"${DigestUtil.hexDigest(it)}\""
@@ -948,15 +949,15 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
948949

949950
@Test
950951
@S3VerifiedSuccess(year = 2025)
951-
fun `PUT object fails with non matching etag`(testInfo: TestInfo) {
952+
fun `PUT object fails with if-match=false`(testInfo: TestInfo) {
952953
val uploadFile = File(UPLOAD_FILE_NAME)
953954
val expectedEtag = FileInputStream(uploadFile).let {
954955
"\"${DigestUtil.hexDigest(it)}\""
955956
}
956957
val nonMatchingEtag = "\"$randomName\""
957958

958959
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
959-
val eTag = putObjectResponse.eTag().also {
960+
putObjectResponse.eTag().also {
960961
assertThat(it).isEqualTo(expectedEtag)
961962
}
962963

@@ -975,14 +976,14 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
975976
@Test
976977
@S3VerifiedFailure(year = 2025,
977978
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
978-
fun `DELETE object succeeds with matching etag`(testInfo: TestInfo) {
979+
fun `DELETE object succeeds with if-match=true`(testInfo: TestInfo) {
979980
val uploadFile = File(UPLOAD_FILE_NAME)
980981
val expectedEtag = FileInputStream(uploadFile).let {
981982
"\"${DigestUtil.hexDigest(it)}\""
982983
}
983984

984985
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
985-
val eTag = putObjectResponse.eTag().also {
986+
putObjectResponse.eTag().also {
986987
assertThat(it).isEqualTo(expectedEtag)
987988
}
988989

@@ -996,7 +997,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
996997
@Test
997998
@S3VerifiedFailure(year = 2025,
998999
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
999-
fun `DELETE object succeeds with matching wildcard etag`(testInfo: TestInfo) {
1000+
fun `DELETE object succeeds with if-match=true with wildcard`(testInfo: TestInfo) {
10001001
val uploadFile = File(UPLOAD_FILE_NAME)
10011002
val expectedEtag = FileInputStream(uploadFile).let {
10021003
"\"${DigestUtil.hexDigest(it)}\""
@@ -1005,7 +1006,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
10051006
val matchingEtag = WILDCARD
10061007

10071008
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1008-
val eTag = putObjectResponse.eTag().also {
1009+
putObjectResponse.eTag().also {
10091010
assertThat(it).isEqualTo(expectedEtag)
10101011
}
10111012

@@ -1019,14 +1020,14 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
10191020
@Test
10201021
@S3VerifiedFailure(year = 2025,
10211022
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
1022-
fun `DELETE object succeeds with matching size`(testInfo: TestInfo) {
1023+
fun `DELETE object succeeds with if-match-size=true`(testInfo: TestInfo) {
10231024
val uploadFile = File(UPLOAD_FILE_NAME)
10241025
val expectedEtag = FileInputStream(uploadFile).let {
10251026
"\"${DigestUtil.hexDigest(it)}\""
10261027
}
10271028

10281029
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1029-
val eTag = putObjectResponse.eTag().also {
1030+
putObjectResponse.eTag().also {
10301031
assertThat(it).isEqualTo(expectedEtag)
10311032
}
10321033

@@ -1041,7 +1042,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
10411042
@Test
10421043
@S3VerifiedFailure(year = 2025,
10431044
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
1044-
fun `DELETE object succeeds with matching lastModifiedTime`(testInfo: TestInfo) {
1045+
fun `DELETE object succeeds with if-match-last-modified-time=true`(testInfo: TestInfo) {
10451046
val (bucketName, _) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
10461047

10471048
val lastModified = s3Client.headObject {
@@ -1059,15 +1060,15 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
10591060
@Test
10601061
@S3VerifiedFailure(year = 2025,
10611062
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
1062-
fun `DELETE object fails with non matching etag`(testInfo: TestInfo) {
1063+
fun `DELETE object fails with if-match=false`(testInfo: TestInfo) {
10631064
val uploadFile = File(UPLOAD_FILE_NAME)
10641065
val expectedEtag = FileInputStream(uploadFile).let {
10651066
"\"${DigestUtil.hexDigest(it)}\""
10661067
}
10671068
val nonMatchingEtag = "\"$randomName\""
10681069

10691070
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1070-
val eTag = putObjectResponse.eTag().also {
1071+
putObjectResponse.eTag().also {
10711072
assertThat(it).isEqualTo(expectedEtag)
10721073
}
10731074

@@ -1084,15 +1085,15 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
10841085
@Test
10851086
@S3VerifiedFailure(year = 2025,
10861087
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
1087-
fun `DELETE object fails with non matching size`(testInfo: TestInfo) {
1088+
fun `DELETE object fails with if-match-size=false`(testInfo: TestInfo) {
10881089
val uploadFile = File(UPLOAD_FILE_NAME)
10891090
val expectedEtag = FileInputStream(uploadFile).let {
10901091
"\"${DigestUtil.hexDigest(it)}\""
10911092
}
10921093
val nonMatchingSize = 0L
10931094

10941095
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1095-
val eTag = putObjectResponse.eTag().also {
1096+
putObjectResponse.eTag().also {
10961097
assertThat(it).isEqualTo(expectedEtag)
10971098
}
10981099

@@ -1109,15 +1110,15 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
11091110
@Test
11101111
@S3VerifiedFailure(year = 2025,
11111112
reason = "Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented.")
1112-
fun `DELETE object fails with non matching lastModifiedTime`(testInfo: TestInfo) {
1113+
fun `DELETE object fails with if-match-last-modified-time=false`(testInfo: TestInfo) {
11131114
val uploadFile = File(UPLOAD_FILE_NAME)
11141115
val expectedEtag = FileInputStream(uploadFile).let {
11151116
"\"${DigestUtil.hexDigest(it)}\""
11161117
}
11171118
val lastModifiedTime = Instant.now().minusSeconds(60)
11181119

11191120
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1120-
val eTag = putObjectResponse.eTag().also {
1121+
putObjectResponse.eTag().also {
11211122
assertThat(it).isEqualTo(expectedEtag)
11221123
}
11231124

@@ -1133,7 +1134,79 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
11331134

11341135
@Test
11351136
@S3VerifiedSuccess(year = 2025)
1136-
fun testHeadObject_successWithNonMatchEtag(testInfo: TestInfo) {
1137+
fun `HEAD object succeeds with if-match=true`(testInfo: TestInfo) {
1138+
val uploadFile = File(UPLOAD_FILE_NAME)
1139+
val expectedEtag = FileInputStream(uploadFile).let {
1140+
"\"${DigestUtil.hexDigest(it)}\""
1141+
}
1142+
1143+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1144+
val eTag = putObjectResponse.eTag().also {
1145+
assertThat(it).isEqualTo(expectedEtag)
1146+
}
1147+
1148+
s3Client.headObject {
1149+
it.bucket(bucketName)
1150+
it.key(UPLOAD_FILE_NAME)
1151+
it.ifMatch(expectedEtag)
1152+
}.also {
1153+
assertThat(it.eTag()).isEqualTo(eTag)
1154+
}
1155+
}
1156+
1157+
@Disabled("Spring Boot sends a 412 for this request even though the controller returns a 200 OK." +
1158+
"This test succeeds against the AWS S3 API.")
1159+
@Test
1160+
@S3VerifiedSuccess(year = 2025)
1161+
fun `HEAD object succeeds with if-match=true and if-unmodified-since=false`(testInfo: TestInfo) {
1162+
val now = Instant.now().minusSeconds(60)
1163+
val uploadFile = File(UPLOAD_FILE_NAME)
1164+
val expectedEtag = FileInputStream(uploadFile).let {
1165+
"\"${DigestUtil.hexDigest(it)}\""
1166+
}
1167+
1168+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1169+
val eTag = putObjectResponse.eTag().also {
1170+
assertThat(it).isEqualTo(expectedEtag)
1171+
}
1172+
1173+
s3Client.headObject {
1174+
it.bucket(bucketName)
1175+
it.key(UPLOAD_FILE_NAME)
1176+
it.ifMatch(expectedEtag)
1177+
it.ifUnmodifiedSince(now)
1178+
}.also {
1179+
assertThat(it.eTag()).isEqualTo(eTag)
1180+
}
1181+
}
1182+
1183+
@Test
1184+
@S3VerifiedSuccess(year = 2025)
1185+
fun `HEAD object fails with if-match=false`(testInfo: TestInfo) {
1186+
val expectedEtag = FileInputStream(File(UPLOAD_FILE_NAME)).let {
1187+
"\"${DigestUtil.hexDigest(it)}\""
1188+
}
1189+
1190+
val nonMatchingEtag = "\"$randomName\""
1191+
1192+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1193+
putObjectResponse.eTag().also {
1194+
assertThat(it).isEqualTo(expectedEtag)
1195+
}
1196+
1197+
assertThatThrownBy {
1198+
s3Client.headObject {
1199+
it.bucket(bucketName)
1200+
it.key(UPLOAD_FILE_NAME)
1201+
it.ifMatch(nonMatchingEtag)
1202+
}
1203+
}.isInstanceOf(S3Exception::class.java)
1204+
.hasMessageContaining("Service: S3, Status Code: 412")
1205+
}
1206+
1207+
@Test
1208+
@S3VerifiedSuccess(year = 2025)
1209+
fun `HEAD object succeeds with if-none-match=true`(testInfo: TestInfo) {
11371210
val uploadFile = File(UPLOAD_FILE_NAME)
11381211
val expectedEtag = FileInputStream(uploadFile).let {
11391212
"\"${DigestUtil.hexDigest(it)}\""
@@ -1157,7 +1230,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
11571230

11581231
@Test
11591232
@S3VerifiedSuccess(year = 2025)
1160-
fun testHeadObject_failureWithNonMatchWildcardEtag(testInfo: TestInfo) {
1233+
fun `HEAD object fails with if-none-match=false with wildcard`(testInfo: TestInfo) {
11611234
val uploadFile = File(UPLOAD_FILE_NAME)
11621235
val expectedEtag = FileInputStream(uploadFile).let {
11631236
"\"${DigestUtil.hexDigest(it)}\""
@@ -1182,12 +1255,14 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
11821255

11831256
@Test
11841257
@S3VerifiedSuccess(year = 2025)
1185-
fun testHeadObject_failureWithMatchEtag(testInfo: TestInfo) {
1186-
val expectedEtag = FileInputStream(File(UPLOAD_FILE_NAME)).let {
1258+
fun `HEAD object fails with if-modified-since=true and if-none-match=false with wildcard`(testInfo: TestInfo) {
1259+
val now = Instant.now().minusSeconds(60)
1260+
val uploadFile = File(UPLOAD_FILE_NAME)
1261+
val expectedEtag = FileInputStream(uploadFile).let {
11871262
"\"${DigestUtil.hexDigest(it)}\""
11881263
}
11891264

1190-
val nonMatchingEtag = "\"$randomName\""
1265+
val nonMatchingEtag = WILDCARD
11911266

11921267
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
11931268
putObjectResponse.eTag().also {
@@ -1198,7 +1273,105 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
11981273
s3Client.headObject {
11991274
it.bucket(bucketName)
12001275
it.key(UPLOAD_FILE_NAME)
1201-
it.ifMatch(nonMatchingEtag)
1276+
it.ifModifiedSince(now)
1277+
it.ifNoneMatch(nonMatchingEtag)
1278+
}
1279+
}.isInstanceOf(S3Exception::class.java)
1280+
.hasMessageContaining("Service: S3, Status Code: 304")
1281+
}
1282+
1283+
@Test
1284+
@S3VerifiedSuccess(year = 2025)
1285+
fun `HEAD object succeeds with if-modified-since=true`(testInfo: TestInfo) {
1286+
val now = Instant.now().minusSeconds(60)
1287+
val uploadFile = File(UPLOAD_FILE_NAME)
1288+
val expectedEtag = FileInputStream(uploadFile).let {
1289+
"\"${DigestUtil.hexDigest(it)}\""
1290+
}
1291+
1292+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1293+
val eTag = putObjectResponse.eTag().also {
1294+
assertThat(it).isEqualTo(expectedEtag)
1295+
}
1296+
1297+
s3Client.headObject {
1298+
it.bucket(bucketName)
1299+
it.key(UPLOAD_FILE_NAME)
1300+
it.ifModifiedSince(now)
1301+
}.also {
1302+
assertThat(it.eTag()).isEqualTo(eTag)
1303+
}
1304+
}
1305+
1306+
@Test
1307+
@S3VerifiedSuccess(year = 2025)
1308+
fun `HEAD object fails with if-modified-since=false`(testInfo: TestInfo) {
1309+
val uploadFile = File(UPLOAD_FILE_NAME)
1310+
val expectedEtag = FileInputStream(uploadFile).let {
1311+
"\"${DigestUtil.hexDigest(it)}\""
1312+
}
1313+
1314+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1315+
putObjectResponse.eTag().also {
1316+
assertThat(it).isEqualTo(expectedEtag)
1317+
}
1318+
1319+
val now = Instant.now().plusSeconds(60)
1320+
1321+
assertThatThrownBy {
1322+
s3Client.headObject {
1323+
it.bucket(bucketName)
1324+
it.key(UPLOAD_FILE_NAME)
1325+
it.ifModifiedSince(now)
1326+
}
1327+
}.isInstanceOf(S3Exception::class.java)
1328+
.hasMessageContaining("Service: S3, Status Code: 304")
1329+
}
1330+
1331+
@Test
1332+
@S3VerifiedSuccess(year = 2025)
1333+
fun `HEAD object succeeds with if-unmodified-since=true`(testInfo: TestInfo) {
1334+
val uploadFile = File(UPLOAD_FILE_NAME)
1335+
val expectedEtag = FileInputStream(uploadFile).let {
1336+
"\"${DigestUtil.hexDigest(it)}\""
1337+
}
1338+
1339+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1340+
val eTag = putObjectResponse.eTag().also {
1341+
assertThat(it).isEqualTo(expectedEtag)
1342+
}
1343+
1344+
val now = Instant.now().plusSeconds(60)
1345+
1346+
s3Client.headObject {
1347+
it.bucket(bucketName)
1348+
it.key(UPLOAD_FILE_NAME)
1349+
it.ifUnmodifiedSince(now)
1350+
}.also {
1351+
assertThat(it.eTag()).isEqualTo(eTag)
1352+
}
1353+
}
1354+
1355+
@Test
1356+
@S3VerifiedSuccess(year = 2025)
1357+
fun `HEAD object fails with if-unmodified-since=false`(testInfo: TestInfo) {
1358+
val now = Instant.now().minusSeconds(60)
1359+
val uploadFile = File(UPLOAD_FILE_NAME)
1360+
val expectedEtag = FileInputStream(uploadFile).let {
1361+
"\"${DigestUtil.hexDigest(it)}\""
1362+
}
1363+
1364+
val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME)
1365+
putObjectResponse.eTag().also {
1366+
assertThat(it).isEqualTo(expectedEtag)
1367+
}
1368+
1369+
1370+
assertThatThrownBy {
1371+
s3Client.headObject {
1372+
it.bucket(bucketName)
1373+
it.key(UPLOAD_FILE_NAME)
1374+
it.ifUnmodifiedSince(now)
12021375
}
12031376
}.isInstanceOf(S3Exception::class.java)
12041377
.hasMessageContaining("Service: S3, Status Code: 412")

0 commit comments

Comments
 (0)