@@ -901,7 +901,7 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
901901
902902 @Test
903903 @S3VerifiedSuccess(year = 2025 )
904- fun testGetObject_successWithMatchingEtag (testInfo : TestInfo ) {
904+ fun `GET object succeeds with matching etag` (testInfo : TestInfo ) {
905905 val uploadFile = File (UPLOAD_FILE_NAME )
906906 val matchingEtag = FileInputStream (uploadFile).let {
907907 " \" ${DigestUtil .hexDigest(it)} \" "
@@ -955,6 +955,239 @@ internal class GetPutDeleteObjectIT : S3TestBase() {
955955 }
956956 }
957957
958+ @Test
959+ @S3VerifiedSuccess(year = 2025 )
960+ fun `PUT object succeeds with matching etag` (testInfo : TestInfo ) {
961+ val uploadFile = File (UPLOAD_FILE_NAME )
962+ val matchingEtag = FileInputStream (uploadFile).let {
963+ " \" ${DigestUtil .hexDigest(it)} \" "
964+ }
965+
966+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
967+ putObjectResponse.eTag().also {
968+ assertThat(it).isEqualTo(matchingEtag)
969+ }
970+
971+ s3Client.putObject ({
972+ it.bucket(bucketName)
973+ it.key(UPLOAD_FILE_NAME )
974+ it.ifMatch(matchingEtag)
975+ }, RequestBody .fromFile(uploadFile)
976+ )
977+ }
978+
979+ @Test
980+ @S3VerifiedFailure(year = 2025 ,
981+ reason = " S3 returns: A header you provided implies functionality that is not implemented." )
982+ fun `PUT object fails with non matching wildcard etag` (testInfo : TestInfo ) {
983+ val uploadFile = File (UPLOAD_FILE_NAME )
984+ val expectedEtag = FileInputStream (uploadFile).let {
985+ " \" ${DigestUtil .hexDigest(it)} \" "
986+ }
987+ val nonMatchingEtag = " \" *\" "
988+
989+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
990+ putObjectResponse.eTag().also {
991+ assertThat(it).isEqualTo(expectedEtag)
992+ }
993+
994+ assertThatThrownBy {
995+ s3Client.putObject(
996+ {
997+ it.bucket(bucketName)
998+ it.key(UPLOAD_FILE_NAME )
999+ it.ifNoneMatch(nonMatchingEtag)
1000+ }, RequestBody .fromFile(uploadFile)
1001+ )
1002+ }.isInstanceOf(S3Exception ::class .java)
1003+ .hasMessageContaining(" Service: S3, Status Code: 304" )
1004+ }
1005+
1006+ @Test
1007+ @S3VerifiedSuccess(year = 2025 )
1008+ fun `PUT object fails with non matching etag` (testInfo : TestInfo ) {
1009+ val uploadFile = File (UPLOAD_FILE_NAME )
1010+ val expectedEtag = FileInputStream (uploadFile).let {
1011+ " \" ${DigestUtil .hexDigest(it)} \" "
1012+ }
1013+ val nonMatchingEtag = " \" $randomName \" "
1014+
1015+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1016+ val eTag = putObjectResponse.eTag().also {
1017+ assertThat(it).isEqualTo(expectedEtag)
1018+ }
1019+
1020+ assertThatThrownBy {
1021+ s3Client.putObject(
1022+ {
1023+ it.bucket(bucketName)
1024+ it.key(UPLOAD_FILE_NAME )
1025+ it.ifMatch(nonMatchingEtag)
1026+ }, RequestBody .fromFile(uploadFile)
1027+ )
1028+ }.isInstanceOf(S3Exception ::class .java)
1029+ .hasMessageContaining(" Service: S3, Status Code: 412" )
1030+ }
1031+
1032+ @Test
1033+ @S3VerifiedFailure(year = 2025 ,
1034+ reason = " Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented." )
1035+ fun `DELETE object succeeds with matching etag` (testInfo : TestInfo ) {
1036+ val uploadFile = File (UPLOAD_FILE_NAME )
1037+ val expectedEtag = FileInputStream (uploadFile).let {
1038+ " \" ${DigestUtil .hexDigest(it)} \" "
1039+ }
1040+
1041+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1042+ val eTag = putObjectResponse.eTag().also {
1043+ assertThat(it).isEqualTo(expectedEtag)
1044+ }
1045+
1046+ s3Client.deleteObject {
1047+ it.bucket(bucketName)
1048+ it.key(UPLOAD_FILE_NAME )
1049+ it.ifMatch(expectedEtag)
1050+ }
1051+ }
1052+
1053+ @Test
1054+ @S3VerifiedFailure(year = 2025 ,
1055+ reason = " Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented." )
1056+ fun `DELETE object succeeds with matching wildcard etag` (testInfo : TestInfo ) {
1057+ val uploadFile = File (UPLOAD_FILE_NAME )
1058+ val expectedEtag = FileInputStream (uploadFile).let {
1059+ " \" ${DigestUtil .hexDigest(it)} \" "
1060+ }
1061+
1062+ val matchingEtag = " \" *\" "
1063+
1064+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1065+ val eTag = putObjectResponse.eTag().also {
1066+ assertThat(it).isEqualTo(expectedEtag)
1067+ }
1068+
1069+ s3Client.deleteObject {
1070+ it.bucket(bucketName)
1071+ it.key(UPLOAD_FILE_NAME )
1072+ it.ifMatch(matchingEtag)
1073+ }
1074+ }
1075+
1076+ @Test
1077+ @S3VerifiedFailure(year = 2025 ,
1078+ reason = " Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented." )
1079+ fun `DELETE object succeeds with matching size` (testInfo : TestInfo ) {
1080+ val uploadFile = File (UPLOAD_FILE_NAME )
1081+ val expectedEtag = FileInputStream (uploadFile).let {
1082+ " \" ${DigestUtil .hexDigest(it)} \" "
1083+ }
1084+
1085+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1086+ val eTag = putObjectResponse.eTag().also {
1087+ assertThat(it).isEqualTo(expectedEtag)
1088+ }
1089+
1090+ s3Client.deleteObject {
1091+ it.bucket(bucketName)
1092+ it.key(UPLOAD_FILE_NAME )
1093+ it.ifMatchSize(uploadFile.length())
1094+ }
1095+ }
1096+
1097+
1098+ @Test
1099+ @S3VerifiedFailure(year = 2025 ,
1100+ reason = " Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented." )
1101+ fun `DELETE object succeeds with matching lastModifiedTime` (testInfo : TestInfo ) {
1102+ val (bucketName, _) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1103+
1104+ val lastModified = s3Client.headObject {
1105+ it.bucket(bucketName)
1106+ it.key(UPLOAD_FILE_NAME )
1107+ }.lastModified()
1108+
1109+ s3Client.deleteObject {
1110+ it.bucket(bucketName)
1111+ it.key(UPLOAD_FILE_NAME )
1112+ it.ifMatchLastModifiedTime(lastModified)
1113+ }
1114+ }
1115+
1116+ @Test
1117+ @S3VerifiedFailure(year = 2025 ,
1118+ reason = " Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented." )
1119+ fun `DELETE object fails with non matching etag` (testInfo : TestInfo ) {
1120+ val uploadFile = File (UPLOAD_FILE_NAME )
1121+ val expectedEtag = FileInputStream (uploadFile).let {
1122+ " \" ${DigestUtil .hexDigest(it)} \" "
1123+ }
1124+ val nonMatchingEtag = " \" $randomName \" "
1125+
1126+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1127+ val eTag = putObjectResponse.eTag().also {
1128+ assertThat(it).isEqualTo(expectedEtag)
1129+ }
1130+
1131+ assertThatThrownBy {
1132+ s3Client.deleteObject {
1133+ it.bucket(bucketName)
1134+ it.key(UPLOAD_FILE_NAME )
1135+ it.ifMatch(nonMatchingEtag)
1136+ }
1137+ }.isInstanceOf(S3Exception ::class .java)
1138+ .hasMessageContaining(" Service: S3, Status Code: 412" )
1139+ }
1140+
1141+ @Test
1142+ @S3VerifiedFailure(year = 2025 ,
1143+ reason = " Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented." )
1144+ fun `DELETE object fails with non matching size` (testInfo : TestInfo ) {
1145+ val uploadFile = File (UPLOAD_FILE_NAME )
1146+ val expectedEtag = FileInputStream (uploadFile).let {
1147+ " \" ${DigestUtil .hexDigest(it)} \" "
1148+ }
1149+ val nonMatchingSize = 0L
1150+
1151+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1152+ val eTag = putObjectResponse.eTag().also {
1153+ assertThat(it).isEqualTo(expectedEtag)
1154+ }
1155+
1156+ assertThatThrownBy {
1157+ s3Client.deleteObject {
1158+ it.bucket(bucketName)
1159+ it.key(UPLOAD_FILE_NAME )
1160+ it.ifMatchSize(nonMatchingSize)
1161+ }
1162+ }.isInstanceOf(S3Exception ::class .java)
1163+ .hasMessageContaining(" Service: S3, Status Code: 412" )
1164+ }
1165+
1166+ @Test
1167+ @S3VerifiedFailure(year = 2025 ,
1168+ reason = " Supported only on directory buckets. S3 returns: A header you provided implies functionality that is not implemented." )
1169+ fun `DELETE object fails with non matching lastModifiedTime` (testInfo : TestInfo ) {
1170+ val uploadFile = File (UPLOAD_FILE_NAME )
1171+ val expectedEtag = FileInputStream (uploadFile).let {
1172+ " \" ${DigestUtil .hexDigest(it)} \" "
1173+ }
1174+ val lastModifiedTime = Instant .now().minusSeconds(60 )
1175+
1176+ val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME )
1177+ val eTag = putObjectResponse.eTag().also {
1178+ assertThat(it).isEqualTo(expectedEtag)
1179+ }
1180+
1181+ assertThatThrownBy {
1182+ s3Client.deleteObject {
1183+ it.bucket(bucketName)
1184+ it.key(UPLOAD_FILE_NAME )
1185+ it.ifMatchLastModifiedTime(lastModifiedTime)
1186+ }
1187+ }.isInstanceOf(S3Exception ::class .java)
1188+ .hasMessageContaining(" Service: S3, Status Code: 412" )
1189+ }
1190+
9581191 @Test
9591192 @S3VerifiedSuccess(year = 2025 )
9601193 fun testHeadObject_successWithNonMatchEtag (testInfo : TestInfo ) {
0 commit comments