@@ -19,6 +19,7 @@ package com.adobe.testing.s3mock.its
1919import com.adobe.testing.s3mock.util.DigestUtil
2020import org.assertj.core.api.Assertions.assertThat
2121import org.assertj.core.api.Assertions.assertThatThrownBy
22+ import org.junit.jupiter.api.Disabled
2223import org.junit.jupiter.api.Test
2324import org.junit.jupiter.api.TestInfo
2425import 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