github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/bucket/lifecycle/lifecycle_test.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package lifecycle 19 20 import ( 21 "bytes" 22 "encoding/xml" 23 "fmt" 24 "net/http" 25 "net/http/httptest" 26 "strconv" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/dustin/go-humanize" 32 "github.com/google/uuid" 33 xhttp "github.com/minio/minio/internal/http" 34 ) 35 36 func TestParseAndValidateLifecycleConfig(t *testing.T) { 37 testCases := []struct { 38 inputConfig string 39 expectedParsingErr error 40 expectedValidationErr error 41 }{ 42 { // Valid lifecycle config 43 inputConfig: `<LifecycleConfiguration> 44 <Rule> 45 <ID>testRule1</ID> 46 <Filter> 47 <Prefix>prefix</Prefix> 48 </Filter> 49 <Status>Enabled</Status> 50 <Expiration><Days>3</Days></Expiration> 51 </Rule> 52 <Rule> 53 <ID>testRule2</ID> 54 <Filter> 55 <Prefix>another-prefix</Prefix> 56 </Filter> 57 <Status>Enabled</Status> 58 <Expiration><Days>3</Days></Expiration> 59 </Rule> 60 </LifecycleConfiguration>`, 61 expectedParsingErr: nil, 62 expectedValidationErr: nil, 63 }, 64 { // Valid lifecycle config 65 inputConfig: `<LifecycleConfiguration> 66 <Rule> 67 <Filter> 68 <And><Tag><Key>key1</Key><Value>val1</Value><Key>key2</Key><Value>val2</Value></Tag></And> 69 </Filter> 70 <Expiration><Days>3</Days></Expiration> 71 </Rule> 72 </LifecycleConfiguration>`, 73 expectedParsingErr: errDuplicatedXMLTag, 74 expectedValidationErr: nil, 75 }, 76 { // lifecycle config with no rules 77 inputConfig: `<LifecycleConfiguration> 78 </LifecycleConfiguration>`, 79 expectedParsingErr: nil, 80 expectedValidationErr: errLifecycleNoRule, 81 }, 82 { // lifecycle config with rules having overlapping prefix 83 inputConfig: `<LifecycleConfiguration><Rule><ID>rule1</ID><Status>Enabled</Status><Filter><Prefix>/a/b</Prefix></Filter><Expiration><Days>3</Days></Expiration></Rule><Rule><ID>rule2</ID><Status>Enabled</Status><Filter><And><Prefix>/a/b/c</Prefix><Tag><Key>key1</Key><Value>val1</Value></Tag></And></Filter><Expiration><Days>3</Days></Expiration></Rule></LifecycleConfiguration> `, 84 expectedParsingErr: nil, 85 expectedValidationErr: nil, 86 }, 87 { // lifecycle config with rules having duplicate ID 88 inputConfig: `<LifecycleConfiguration><Rule><ID>duplicateID</ID><Status>Enabled</Status><Filter><Prefix>/a/b</Prefix></Filter><Expiration><Days>3</Days></Expiration></Rule><Rule><ID>duplicateID</ID><Status>Enabled</Status><Filter><And><Prefix>/x/z</Prefix><Tag><Key>key1</Key><Value>val1</Value></Tag></And></Filter><Expiration><Days>4</Days></Expiration></Rule></LifecycleConfiguration>`, 89 expectedParsingErr: nil, 90 expectedValidationErr: errLifecycleDuplicateID, 91 }, 92 // Missing <Tag> in <And> 93 { 94 inputConfig: `<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><ID>sample-rule-2</ID><Filter><And><Prefix>/a/b/c</Prefix></And></Filter><Status>Enabled</Status><Expiration><Days>1</Days></Expiration></Rule></LifecycleConfiguration>`, 95 expectedParsingErr: nil, 96 expectedValidationErr: errXMLNotWellFormed, 97 }, 98 // Lifecycle with the deprecated Prefix tag 99 { 100 inputConfig: `<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><ID>rule</ID><Prefix /><Status>Enabled</Status><Expiration><Days>1</Days></Expiration></Rule></LifecycleConfiguration>`, 101 expectedParsingErr: nil, 102 expectedValidationErr: nil, 103 }, 104 // Lifecycle with empty Filter tag 105 { 106 inputConfig: `<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><ID>rule</ID><Filter></Filter><Status>Enabled</Status><Expiration><Days>1</Days></Expiration></Rule></LifecycleConfiguration>`, 107 expectedParsingErr: nil, 108 expectedValidationErr: nil, 109 }, 110 // Lifecycle with zero Transition Days 111 { 112 inputConfig: `<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><ID>rule</ID><Filter></Filter><Status>Enabled</Status><Transition><Days>0</Days><StorageClass>S3TIER-1</StorageClass></Transition></Rule></LifecycleConfiguration>`, 113 expectedParsingErr: nil, 114 expectedValidationErr: nil, 115 }, 116 // Lifecycle with max noncurrent versions 117 { 118 inputConfig: `<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><ID>rule</ID>><Status>Enabled</Status><Filter></Filter><NoncurrentVersionExpiration><NewerNoncurrentVersions>5</NewerNoncurrentVersions></NoncurrentVersionExpiration></Rule></LifecycleConfiguration>`, 119 expectedParsingErr: nil, 120 expectedValidationErr: nil, 121 }, 122 } 123 124 for i, tc := range testCases { 125 t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) { 126 lc, err := ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))) 127 if err != tc.expectedParsingErr { 128 t.Fatalf("%d: Expected %v during parsing but got %v", i+1, tc.expectedParsingErr, err) 129 } 130 if tc.expectedParsingErr != nil { 131 // We already expect a parsing error, 132 // no need to continue this test. 133 return 134 } 135 err = lc.Validate() 136 if err != tc.expectedValidationErr { 137 t.Fatalf("%d: Expected %v during validation but got %v", i+1, tc.expectedValidationErr, err) 138 } 139 }) 140 } 141 } 142 143 // TestMarshalLifecycleConfig checks if lifecycleconfig xml 144 // marshaling/unmarshaling can handle output from each other 145 func TestMarshalLifecycleConfig(t *testing.T) { 146 // Time at midnight UTC 147 midnightTS := ExpirationDate{time.Date(2019, time.April, 20, 0, 0, 0, 0, time.UTC)} 148 lc := Lifecycle{ 149 Rules: []Rule{ 150 { 151 Status: "Enabled", 152 Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}}, 153 Expiration: Expiration{Days: ExpirationDays(3)}, 154 }, 155 { 156 Status: "Enabled", 157 Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}}, 158 Expiration: Expiration{Date: midnightTS}, 159 }, 160 { 161 Status: "Enabled", 162 Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}}, 163 Expiration: Expiration{Date: midnightTS}, 164 NoncurrentVersionTransition: NoncurrentVersionTransition{NoncurrentDays: TransitionDays(2), StorageClass: "TEST"}, 165 }, 166 }, 167 } 168 b, err := xml.MarshalIndent(&lc, "", "\t") 169 if err != nil { 170 t.Fatal(err) 171 } 172 var lc1 Lifecycle 173 err = xml.Unmarshal(b, &lc1) 174 if err != nil { 175 t.Fatal(err) 176 } 177 178 ruleSet := make(map[string]struct{}) 179 for _, rule := range lc.Rules { 180 ruleBytes, err := xml.Marshal(rule) 181 if err != nil { 182 t.Fatal(err) 183 } 184 ruleSet[string(ruleBytes)] = struct{}{} 185 } 186 for _, rule := range lc1.Rules { 187 ruleBytes, err := xml.Marshal(rule) 188 if err != nil { 189 t.Fatal(err) 190 } 191 if _, ok := ruleSet[string(ruleBytes)]; !ok { 192 t.Fatalf("Expected %v to be equal to %v, %v missing", lc, lc1, rule) 193 } 194 } 195 } 196 197 func TestExpectedExpiryTime(t *testing.T) { 198 testCases := []struct { 199 modTime time.Time 200 days ExpirationDays 201 expected time.Time 202 }{ 203 { 204 time.Date(2020, time.March, 15, 10, 10, 10, 0, time.UTC), 205 4, 206 time.Date(2020, time.March, 20, 0, 0, 0, 0, time.UTC), 207 }, 208 { 209 time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC), 210 1, 211 time.Date(2020, time.March, 17, 0, 0, 0, 0, time.UTC), 212 }, 213 } 214 215 for i, tc := range testCases { 216 t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) { 217 got := ExpectedExpiryTime(tc.modTime, int(tc.days)) 218 if !got.Equal(tc.expected) { 219 t.Fatalf("Expected %v to be equal to %v", got, tc.expected) 220 } 221 }) 222 } 223 } 224 225 func TestEval(t *testing.T) { 226 testCases := []struct { 227 inputConfig string 228 objectName string 229 objectTags string 230 objectModTime time.Time 231 isExpiredDelMarker bool 232 expectedAction Action 233 isNoncurrent bool 234 objectSuccessorModTime time.Time 235 versionID string 236 }{ 237 // Empty object name (unexpected case) should always return NoneAction 238 { 239 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>prefix</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 240 expectedAction: NoneAction, 241 }, 242 // Disabled should always return NoneAction 243 { 244 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Disabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 245 objectName: "foodir/fooobject", 246 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 247 expectedAction: NoneAction, 248 }, 249 // No modTime, should be none-action 250 { 251 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 252 objectName: "foodir/fooobject", 253 expectedAction: NoneAction, 254 }, 255 // Prefix not matched 256 { 257 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 258 objectName: "foxdir/fooobject", 259 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 260 expectedAction: NoneAction, 261 }, 262 // Test rule with empty prefix e.g. for whole bucket 263 { 264 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix></Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 265 objectName: "foxdir/fooobject/foo.txt", 266 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 267 expectedAction: DeleteAction, 268 }, 269 // Too early to remove (test Days) 270 { 271 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 272 objectName: "foxdir/fooobject", 273 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 274 expectedAction: NoneAction, 275 }, 276 // Should remove (test Days) 277 { 278 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 279 objectName: "foodir/fooobject", 280 objectModTime: time.Now().UTC().Add(-6 * 24 * time.Hour), // Created 6 days ago 281 expectedAction: DeleteAction, 282 }, 283 // Too early to remove (test Date) 284 { 285 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().UTC().Truncate(24*time.Hour).Add(24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 286 objectName: "foodir/fooobject", 287 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 288 expectedAction: NoneAction, 289 }, 290 // Should remove (test Days) 291 { 292 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().UTC().Truncate(24*time.Hour).Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 293 objectName: "foodir/fooobject", 294 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 295 expectedAction: DeleteAction, 296 }, 297 // Should remove (Tags match) 298 { 299 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().UTC().Truncate(24*time.Hour).Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 300 objectName: "foodir/fooobject", 301 objectTags: "tag1=value1&tag2=value2", 302 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 303 expectedAction: DeleteAction, 304 }, 305 // Should remove (Multiple Rules, Tags match) 306 { 307 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value></Tag><Tag><Key>tag2</Key><Value>value2</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule><Rule><Filter><And><Prefix>abc/</Prefix><Tag><Key>tag2</Key><Value>value</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 308 objectName: "foodir/fooobject", 309 objectTags: "tag1=value1&tag2=value2", 310 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 311 expectedAction: DeleteAction, 312 }, 313 // Should remove (Tags match) 314 { 315 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value></Tag><Tag><Key>tag2</Key><Value>value2</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 316 objectName: "foodir/fooobject", 317 objectTags: "tag1=value1&tag2=value2", 318 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 319 expectedAction: DeleteAction, 320 }, 321 // Should remove (Tags match with inverted order) 322 { 323 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Tag><Key>factory</Key><Value>true</Value></Tag><Tag><Key>storeforever</Key><Value>false</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 324 objectName: "fooobject", 325 objectTags: "storeforever=false&factory=true", 326 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 327 expectedAction: DeleteAction, 328 }, 329 // Should remove (Tags with encoded chars) 330 { 331 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Tag><Key>factory</Key><Value>true</Value></Tag><Tag><Key>store forever</Key><Value>false</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 332 objectName: "fooobject", 333 objectTags: "store+forever=false&factory=true", 334 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 335 expectedAction: DeleteAction, 336 }, 337 338 // Should not remove (Tags don't match) 339 { 340 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().UTC().Truncate(24*time.Hour).Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 341 objectName: "foodir/fooobject", 342 objectTags: "tag1=value1", 343 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 344 expectedAction: NoneAction, 345 }, 346 // Should not remove (Tags match, but prefix doesn't match) 347 { 348 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix>foodir/</Prefix><Tag><Key>tag1</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 349 objectName: "foxdir/fooobject", 350 objectTags: "tag1=value1", 351 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 352 expectedAction: NoneAction, 353 }, 354 // Should remove - empty prefix, tags match, date expiration kicked in 355 { 356 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Tag><Key>tag1</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 357 objectName: "foxdir/fooobject", 358 objectTags: "tag1=value1", 359 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 360 expectedAction: DeleteAction, 361 }, 362 // Should remove - empty prefix, tags match, object is expired based on specified Days 363 { 364 inputConfig: `<LifecycleConfiguration><Rule><Filter><And><Prefix></Prefix><Tag><Key>tag1</Key><Value>value1</Value></Tag></And></Filter><Status>Enabled</Status><Expiration><Days>1</Days></Expiration></Rule></LifecycleConfiguration>`, 365 objectName: "foxdir/fooobject", 366 objectTags: "tag1=value1", 367 objectModTime: time.Now().UTC().Add(-48 * time.Hour), // Created 2 day ago 368 expectedAction: DeleteAction, 369 }, 370 // Should remove, the second rule has expiration kicked in 371 { 372 inputConfig: `<LifecycleConfiguration><Rule><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule><Rule><Filter><Prefix>foxdir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></LifecycleConfiguration>`, 373 objectName: "foxdir/fooobject", 374 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 375 expectedAction: DeleteAction, 376 }, 377 // Should accept BucketLifecycleConfiguration root tag 378 { 379 inputConfig: `<BucketLifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>` + time.Now().Truncate(24*time.Hour).UTC().Add(-24*time.Hour).Format(time.RFC3339) + `</Date></Expiration></Rule></BucketLifecycleConfiguration>`, 380 objectName: "foodir/fooobject", 381 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 382 expectedAction: DeleteAction, 383 }, 384 // Should delete expired delete marker right away 385 { 386 inputConfig: `<BucketLifecycleConfiguration><Rule><Expiration><ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker></Expiration><Filter></Filter><Status>Enabled</Status></Rule></BucketLifecycleConfiguration>`, 387 objectName: "foodir/fooobject", 388 objectModTime: time.Now().UTC().Add(-1 * time.Hour), // Created one hour ago 389 isExpiredDelMarker: true, 390 expectedAction: DeleteVersionAction, 391 }, 392 // Should delete expired object right away with 1 day expiration 393 { 394 inputConfig: `<BucketLifecycleConfiguration><Rule><Expiration><Days>1</Days><ExpiredObjectAllVersions>true</ExpiredObjectAllVersions></Expiration><Filter></Filter><Status>Enabled</Status></Rule></BucketLifecycleConfiguration>`, 395 objectName: "foodir/fooobject", 396 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 397 isExpiredDelMarker: true, 398 expectedAction: DeleteAllVersionsAction, 399 }, 400 401 // Should not delete expired marker if its time has not come yet 402 { 403 inputConfig: `<BucketLifecycleConfiguration><Rule><Filter></Filter><Status>Enabled</Status><Expiration><Days>1</Days></Expiration></Rule></BucketLifecycleConfiguration>`, 404 objectName: "foodir/fooobject", 405 objectModTime: time.Now().UTC().Add(-12 * time.Hour), // Created 12 hours ago 406 isExpiredDelMarker: true, 407 expectedAction: NoneAction, 408 }, 409 // Should delete expired marker since its time has come 410 { 411 inputConfig: `<BucketLifecycleConfiguration><Rule><Filter></Filter><Status>Enabled</Status><Expiration><Days>1</Days></Expiration></Rule></BucketLifecycleConfiguration>`, 412 objectName: "foodir/fooobject", 413 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 414 isExpiredDelMarker: true, 415 expectedAction: DeleteVersionAction, 416 }, 417 // Should transition immediately when Transition days is zero 418 { 419 inputConfig: `<BucketLifecycleConfiguration><Rule><Filter></Filter><Status>Enabled</Status><Transition><Days>0</Days><StorageClass>S3TIER-1</StorageClass></Transition></Rule></BucketLifecycleConfiguration>`, 420 objectName: "foodir/fooobject", 421 objectModTime: time.Now().Add(-1 * time.Nanosecond).UTC(), // Created now 422 expectedAction: TransitionAction, 423 }, 424 // Should transition immediately when NoncurrentVersion Transition days is zero 425 { 426 inputConfig: `<BucketLifecycleConfiguration><Rule><Filter></Filter><Status>Enabled</Status><NoncurrentVersionTransition><NoncurrentDays>0</NoncurrentDays><StorageClass>S3TIER-1</StorageClass></NoncurrentVersionTransition></Rule></BucketLifecycleConfiguration>`, 427 objectName: "foodir/fooobject", 428 objectModTime: time.Now().Add(-1 * time.Nanosecond).UTC(), // Created now 429 expectedAction: TransitionVersionAction, 430 isNoncurrent: true, 431 objectSuccessorModTime: time.Now().Add(-1 * time.Nanosecond).UTC(), 432 versionID: uuid.New().String(), 433 }, 434 // Lifecycle rules with NewerNoncurrentVersions specified must return NoneAction. 435 { 436 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><NoncurrentVersionExpiration><NewerNoncurrentVersions>5</NewerNoncurrentVersions></NoncurrentVersionExpiration></Rule></LifecycleConfiguration>`, 437 objectName: "foodir/fooobject", 438 versionID: uuid.NewString(), 439 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 440 expectedAction: NoneAction, 441 }, 442 // Disabled rules with NewerNoncurrentVersions shouldn't affect outcome. 443 { 444 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><NoncurrentVersionExpiration><NoncurrentDays>5</NoncurrentDays></NoncurrentVersionExpiration></Rule><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Disabled</Status><NoncurrentVersionExpiration><NewerNoncurrentVersions>5</NewerNoncurrentVersions></NoncurrentVersionExpiration></Rule></LifecycleConfiguration>`, 445 objectName: "foodir/fooobject", 446 versionID: uuid.NewString(), 447 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 448 objectSuccessorModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 449 isNoncurrent: true, 450 expectedAction: DeleteVersionAction, 451 }, 452 { 453 inputConfig: `<LifecycleConfiguration> 454 <Rule> 455 <ID>Rule 1</ID> 456 <Filter> 457 </Filter> 458 <Status>Enabled</Status> 459 <Expiration> 460 <Days>365</Days> 461 </Expiration> 462 </Rule> 463 <Rule> 464 <ID>Rule 2</ID> 465 <Filter> 466 <Prefix>logs/</Prefix> 467 </Filter> 468 <Status>Enabled</Status> 469 <Transition> 470 <StorageClass>STANDARD_IA</StorageClass> 471 <Days>30</Days> 472 </Transition> 473 </Rule> 474 </LifecycleConfiguration>`, 475 objectName: "logs/obj-1", 476 objectModTime: time.Now().UTC().Add(-31 * 24 * time.Hour), 477 expectedAction: TransitionAction, 478 }, 479 { 480 inputConfig: `<LifecycleConfiguration> 481 <Rule> 482 <ID>Rule 1</ID> 483 <Filter> 484 <Prefix>logs/</Prefix> 485 </Filter> 486 <Status>Enabled</Status> 487 <Expiration> 488 <Days>365</Days> 489 </Expiration> 490 </Rule> 491 <Rule> 492 <ID>Rule 2</ID> 493 <Filter> 494 <Prefix>logs/</Prefix> 495 </Filter> 496 <Status>Enabled</Status> 497 <Transition> 498 <StorageClass>STANDARD_IA</StorageClass> 499 <Days>365</Days> 500 </Transition> 501 </Rule> 502 </LifecycleConfiguration>`, 503 objectName: "logs/obj-1", 504 objectModTime: time.Now().UTC().Add(-366 * 24 * time.Hour), 505 expectedAction: DeleteAction, 506 }, 507 { 508 inputConfig: `<LifecycleConfiguration> 509 <Rule> 510 <ID>Rule 1</ID> 511 <Filter> 512 <Tag> 513 <Key>tag1</Key> 514 <Value>value1</Value> 515 </Tag> 516 </Filter> 517 <Status>Enabled</Status> 518 <Transition> 519 <StorageClass>GLACIER</StorageClass> 520 <Days>365</Days> 521 </Transition> 522 </Rule> 523 <Rule> 524 <ID>Rule 2</ID> 525 <Filter> 526 <Tag> 527 <Key>tag2</Key> 528 <Value>value2</Value> 529 </Tag> 530 </Filter> 531 <Status>Enabled</Status> 532 <Expiration> 533 <Days>14</Days> 534 </Expiration> 535 </Rule> 536 </LifecycleConfiguration>`, 537 objectName: "obj-1", 538 objectTags: "tag1=value1&tag2=value2", 539 objectModTime: time.Now().UTC().Add(-15 * 24 * time.Hour), 540 expectedAction: DeleteAction, 541 }, 542 { 543 inputConfig: `<LifecycleConfiguration> 544 <Rule> 545 <ID>Rule 1</ID> 546 <Status>Enabled</Status> 547 <Filter></Filter> 548 <Transition> 549 <StorageClass>WARM-1</StorageClass> 550 <Days>30</Days> 551 </Transition> 552 <Expiration> 553 <Days>60</Days> 554 </Expiration> 555 </Rule> 556 </LifecycleConfiguration>`, 557 objectName: "obj-1", 558 objectModTime: time.Now().UTC().Add(-90 * 24 * time.Hour), 559 expectedAction: DeleteAction, 560 }, 561 { 562 inputConfig: `<LifecycleConfiguration> 563 <Rule> 564 <ID>Rule 2</ID> 565 <Filter></Filter> 566 <Status>Enabled</Status> 567 <NoncurrentVersionExpiration> 568 <NoncurrentDays>60</NoncurrentDays> 569 </NoncurrentVersionExpiration> 570 <NoncurrentVersionTransition> 571 <StorageClass>WARM-1</StorageClass> 572 <NoncurrentDays>30</NoncurrentDays> 573 </NoncurrentVersionTransition> 574 </Rule> 575 </LifecycleConfiguration>`, 576 objectName: "obj-1", 577 isNoncurrent: true, 578 objectModTime: time.Now().UTC().Add(-90 * 24 * time.Hour), 579 objectSuccessorModTime: time.Now().UTC().Add(-90 * 24 * time.Hour), 580 expectedAction: DeleteVersionAction, 581 }, 582 } 583 584 for _, tc := range testCases { 585 tc := tc 586 t.Run("", func(t *testing.T) { 587 lc, err := ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))) 588 if err != nil { 589 t.Fatalf("Got unexpected error: %v", err) 590 } 591 if res := lc.Eval(ObjectOpts{ 592 Name: tc.objectName, 593 UserTags: tc.objectTags, 594 ModTime: tc.objectModTime, 595 DeleteMarker: tc.isExpiredDelMarker, 596 NumVersions: 1, 597 IsLatest: !tc.isNoncurrent, 598 SuccessorModTime: tc.objectSuccessorModTime, 599 VersionID: tc.versionID, 600 }); res.Action != tc.expectedAction { 601 t.Fatalf("Expected action: `%v`, got: `%v`", tc.expectedAction, res.Action) 602 } 603 }) 604 } 605 } 606 607 func TestHasActiveRules(t *testing.T) { 608 testCases := []struct { 609 inputConfig string 610 prefix string 611 want bool 612 }{ 613 { 614 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 615 prefix: "foodir/foobject", 616 want: true, 617 }, 618 { // empty prefix 619 inputConfig: `<LifecycleConfiguration><Rule><Status>Enabled</Status><Filter></Filter><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 620 prefix: "foodir/foobject/foo.txt", 621 want: true, 622 }, 623 { 624 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 625 prefix: "zdir/foobject", 626 want: false, 627 }, 628 { 629 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/zdir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 630 prefix: "foodir/", 631 want: true, 632 }, 633 { 634 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix></Prefix></Filter><Status>Disabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 635 prefix: "foodir/", 636 want: false, 637 }, 638 { 639 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>2999-01-01T00:00:00.000Z</Date></Expiration></Rule></LifecycleConfiguration>`, 640 prefix: "foodir/foobject", 641 want: false, 642 }, 643 { 644 inputConfig: `<LifecycleConfiguration><Rule><Status>Enabled</Status><Filter></Filter><Transition><StorageClass>S3TIER-1</StorageClass></Transition></Rule></LifecycleConfiguration>`, 645 prefix: "foodir/foobject/foo.txt", 646 want: true, 647 }, 648 { 649 inputConfig: `<LifecycleConfiguration><Rule><Status>Enabled</Status><Filter></Filter><NoncurrentVersionTransition><StorageClass>S3TIER-1</StorageClass></NoncurrentVersionTransition></Rule></LifecycleConfiguration>`, 650 prefix: "foodir/foobject/foo.txt", 651 want: true, 652 }, 653 { 654 inputConfig: `<LifecycleConfiguration><Rule><Status>Enabled</Status><Filter></Filter><Expiration><ExpiredObjectDeleteMarker>true</ExpiredObjectDeleteMarker></Expiration></Rule></LifecycleConfiguration>`, 655 prefix: "", 656 want: true, 657 }, 658 { 659 inputConfig: `<LifecycleConfiguration><Rule><Status>Enabled</Status><Filter></Filter><Expiration><Days>42</Days><ExpiredObjectAllVersions>true</ExpiredObjectAllVersions></Expiration></Rule></LifecycleConfiguration>`, 660 prefix: "", 661 want: true, 662 }, 663 } 664 665 for i, tc := range testCases { 666 tc := tc 667 t.Run(fmt.Sprintf("Test_%d", i+1), func(t *testing.T) { 668 lc, err := ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))) 669 if err != nil { 670 t.Fatalf("Got unexpected error: %v", err) 671 } 672 // To ensure input lifecycle configurations are valid 673 if err := lc.Validate(); err != nil { 674 t.Fatalf("Invalid test case: %d %v", i+1, err) 675 } 676 if got := lc.HasActiveRules(tc.prefix); got != tc.want { 677 t.Fatalf("Expected result: `%v`, got: `%v`", tc.want, got) 678 } 679 }) 680 } 681 } 682 683 func TestSetPredictionHeaders(t *testing.T) { 684 lc := Lifecycle{ 685 Rules: []Rule{ 686 { 687 ID: "rule-1", 688 Status: "Enabled", 689 Expiration: Expiration{ 690 Days: ExpirationDays(3), 691 set: true, 692 }, 693 }, 694 { 695 ID: "rule-2", 696 Status: "Enabled", 697 Transition: Transition{ 698 Days: TransitionDays(3), 699 StorageClass: "TIER-1", 700 set: true, 701 }, 702 }, 703 { 704 ID: "rule-3", 705 Status: "Enabled", 706 NoncurrentVersionTransition: NoncurrentVersionTransition{ 707 NoncurrentDays: TransitionDays(5), 708 StorageClass: "TIER-2", 709 set: true, 710 }, 711 }, 712 }, 713 } 714 715 // current version 716 obj1 := ObjectOpts{ 717 Name: "obj1", 718 IsLatest: true, 719 } 720 // non-current version 721 obj2 := ObjectOpts{ 722 Name: "obj2", 723 } 724 725 tests := []struct { 726 obj ObjectOpts 727 expRuleID int 728 transRuleID int 729 }{ 730 { 731 obj: obj1, 732 expRuleID: 0, 733 transRuleID: 1, 734 }, 735 { 736 obj: obj2, 737 expRuleID: 0, 738 transRuleID: 2, 739 }, 740 } 741 for i, tc := range tests { 742 w := httptest.NewRecorder() 743 lc.SetPredictionHeaders(w, tc.obj) 744 if expHdrs, ok := w.Header()[xhttp.AmzExpiration]; ok && !strings.Contains(expHdrs[0], lc.Rules[tc.expRuleID].ID) { 745 t.Fatalf("Test %d: Expected %s header", i+1, xhttp.AmzExpiration) 746 } 747 if transHdrs, ok := w.Header()[xhttp.MinIOTransition]; ok { 748 if !strings.Contains(transHdrs[0], lc.Rules[tc.transRuleID].ID) { 749 t.Fatalf("Test %d: Expected %s header", i+1, xhttp.MinIOTransition) 750 } 751 752 if tc.obj.IsLatest { 753 if expectedDue, _ := lc.Rules[tc.transRuleID].Transition.NextDue(tc.obj); !strings.Contains(transHdrs[0], expectedDue.Format(http.TimeFormat)) { 754 t.Fatalf("Test %d: Expected transition time %s", i+1, expectedDue) 755 } 756 } else { 757 if expectedDue, _ := lc.Rules[tc.transRuleID].NoncurrentVersionTransition.NextDue(tc.obj); !strings.Contains(transHdrs[0], expectedDue.Format(http.TimeFormat)) { 758 t.Fatalf("Test %d: Expected transition time %s", i+1, expectedDue) 759 } 760 } 761 } 762 } 763 } 764 765 func TestTransitionTier(t *testing.T) { 766 lc := Lifecycle{ 767 Rules: []Rule{ 768 { 769 ID: "rule-1", 770 Status: "Enabled", 771 Transition: Transition{ 772 Days: TransitionDays(3), 773 StorageClass: "TIER-1", 774 }, 775 }, 776 { 777 ID: "rule-2", 778 Status: "Enabled", 779 NoncurrentVersionTransition: NoncurrentVersionTransition{ 780 NoncurrentDays: TransitionDays(3), 781 StorageClass: "TIER-2", 782 }, 783 }, 784 }, 785 } 786 787 now := time.Now().UTC() 788 789 obj1 := ObjectOpts{ 790 Name: "obj1", 791 IsLatest: true, 792 ModTime: now, 793 } 794 795 obj2 := ObjectOpts{ 796 Name: "obj2", 797 ModTime: now, 798 } 799 800 // Go back seven days in the past 801 now = now.Add(7 * 24 * time.Hour) 802 803 evt := lc.eval(obj1, now) 804 if evt.Action != TransitionAction { 805 t.Fatalf("Expected action: %s but got %s", TransitionAction, evt.Action) 806 } 807 if evt.StorageClass != "TIER-1" { 808 t.Fatalf("Expected TIER-1 but got %s", evt.StorageClass) 809 } 810 811 evt = lc.eval(obj2, now) 812 if evt.Action != TransitionVersionAction { 813 t.Fatalf("Expected action: %s but got %s", TransitionVersionAction, evt.Action) 814 } 815 if evt.StorageClass != "TIER-2" { 816 t.Fatalf("Expected TIER-2 but got %s", evt.StorageClass) 817 } 818 } 819 820 func TestTransitionTierWithPrefixAndTags(t *testing.T) { 821 lc := Lifecycle{ 822 Rules: []Rule{ 823 { 824 ID: "rule-1", 825 Status: "Enabled", 826 Filter: Filter{ 827 Prefix: Prefix{ 828 set: true, 829 string: "abcd/", 830 }, 831 }, 832 Transition: Transition{ 833 Days: TransitionDays(3), 834 StorageClass: "TIER-1", 835 }, 836 }, 837 { 838 ID: "rule-2", 839 Status: "Enabled", 840 Filter: Filter{ 841 tagSet: true, 842 Tag: Tag{ 843 Key: "priority", 844 Value: "low", 845 }, 846 }, 847 Transition: Transition{ 848 Days: TransitionDays(3), 849 StorageClass: "TIER-2", 850 }, 851 }, 852 }, 853 } 854 855 now := time.Now().UTC() 856 857 obj1 := ObjectOpts{ 858 Name: "obj1", 859 IsLatest: true, 860 ModTime: now, 861 } 862 863 obj2 := ObjectOpts{ 864 Name: "abcd/obj2", 865 IsLatest: true, 866 ModTime: now, 867 } 868 869 obj3 := ObjectOpts{ 870 Name: "obj3", 871 IsLatest: true, 872 ModTime: now, 873 UserTags: "priority=low", 874 } 875 876 // Go back seven days in the past 877 now = now.Add(7 * 24 * time.Hour) 878 879 // Eval object 1 880 evt := lc.eval(obj1, now) 881 if evt.Action != NoneAction { 882 t.Fatalf("Expected action: %s but got %s", NoneAction, evt.Action) 883 } 884 885 // Eval object 2 886 evt = lc.eval(obj2, now) 887 if evt.Action != TransitionAction { 888 t.Fatalf("Expected action: %s but got %s", TransitionAction, evt.Action) 889 } 890 if evt.StorageClass != "TIER-1" { 891 t.Fatalf("Expected TIER-1 but got %s", evt.StorageClass) 892 } 893 894 // Eval object 3 895 evt = lc.eval(obj3, now) 896 if evt.Action != TransitionAction { 897 t.Fatalf("Expected action: %s but got %s", TransitionAction, evt.Action) 898 } 899 if evt.StorageClass != "TIER-2" { 900 t.Fatalf("Expected TIER-2 but got %s", evt.StorageClass) 901 } 902 } 903 904 func TestNoncurrentVersionsLimit(t *testing.T) { 905 // test that the lowest max noncurrent versions limit is returned among 906 // matching rules 907 var rules []Rule 908 for i := 1; i <= 10; i++ { 909 rules = append(rules, Rule{ 910 ID: strconv.Itoa(i), 911 Status: "Enabled", 912 NoncurrentVersionExpiration: NoncurrentVersionExpiration{ 913 NewerNoncurrentVersions: i, 914 NoncurrentDays: ExpirationDays(i), 915 }, 916 }) 917 } 918 lc := Lifecycle{ 919 Rules: rules, 920 } 921 if event := lc.NoncurrentVersionsExpirationLimit(ObjectOpts{Name: "obj"}); event.RuleID != "1" || event.NoncurrentDays != 1 || event.NewerNoncurrentVersions != 1 { 922 t.Fatalf("Expected (ruleID, days, lim) to be (\"1\", 1, 1) but got (%s, %d, %d)", event.RuleID, event.NoncurrentDays, event.NewerNoncurrentVersions) 923 } 924 } 925 926 func TestMaxNoncurrentBackwardCompat(t *testing.T) { 927 testCases := []struct { 928 xml string 929 expected NoncurrentVersionExpiration 930 }{ 931 { 932 xml: `<NoncurrentVersionExpiration><NoncurrentDays>1</NoncurrentDays><NewerNoncurrentVersions>3</NewerNoncurrentVersions></NoncurrentVersionExpiration>`, 933 expected: NoncurrentVersionExpiration{ 934 XMLName: xml.Name{ 935 Local: "NoncurrentVersionExpiration", 936 }, 937 NoncurrentDays: 1, 938 NewerNoncurrentVersions: 3, 939 set: true, 940 }, 941 }, 942 { 943 xml: `<NoncurrentVersionExpiration><NoncurrentDays>2</NoncurrentDays><MaxNoncurrentVersions>4</MaxNoncurrentVersions></NoncurrentVersionExpiration>`, 944 expected: NoncurrentVersionExpiration{ 945 XMLName: xml.Name{ 946 Local: "NoncurrentVersionExpiration", 947 }, 948 NoncurrentDays: 2, 949 NewerNoncurrentVersions: 4, 950 set: true, 951 }, 952 }, 953 } 954 for i, tc := range testCases { 955 var got NoncurrentVersionExpiration 956 dec := xml.NewDecoder(strings.NewReader(tc.xml)) 957 if err := dec.Decode(&got); err != nil || got != tc.expected { 958 if err != nil { 959 t.Fatalf("%d: Failed to unmarshal xml %v", i+1, err) 960 } 961 t.Fatalf("%d: Expected %v but got %v", i+1, tc.expected, got) 962 } 963 } 964 } 965 966 func TestParseLifecycleConfigWithID(t *testing.T) { 967 r := bytes.NewReader([]byte(`<LifecycleConfiguration> 968 <Rule> 969 <ID>rule-1</ID> 970 <Filter> 971 <Prefix>prefix</Prefix> 972 </Filter> 973 <Status>Enabled</Status> 974 <Expiration><Days>3</Days></Expiration> 975 </Rule> 976 <Rule> 977 <Filter> 978 <Prefix>another-prefix</Prefix> 979 </Filter> 980 <Status>Enabled</Status> 981 <Expiration><Days>3</Days></Expiration> 982 </Rule> 983 </LifecycleConfiguration>`)) 984 lc, err := ParseLifecycleConfigWithID(r) 985 if err != nil { 986 t.Fatalf("Expected parsing to succeed but failed with %v", err) 987 } 988 for _, rule := range lc.Rules { 989 if rule.ID == "" { 990 t.Fatalf("Expected all rules to have a unique id assigned %#v", rule) 991 } 992 } 993 } 994 995 func TestFilterAndSetPredictionHeaders(t *testing.T) { 996 lc := Lifecycle{ 997 Rules: []Rule{ 998 { 999 ID: "rule-1", 1000 Status: "Enabled", 1001 Filter: Filter{ 1002 set: true, 1003 Prefix: Prefix{ 1004 string: "folder1/folder1/exp_dt=2022-", 1005 set: true, 1006 }, 1007 }, 1008 Expiration: Expiration{ 1009 Days: 1, 1010 set: true, 1011 }, 1012 }, 1013 }, 1014 } 1015 tests := []struct { 1016 opts ObjectOpts 1017 lc Lifecycle 1018 want int 1019 }{ 1020 { 1021 opts: ObjectOpts{ 1022 Name: "folder1/folder1/exp_dt=2022-08-01/obj-1", 1023 ModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), 1024 VersionID: "", 1025 IsLatest: true, 1026 NumVersions: 1, 1027 }, 1028 want: 1, 1029 lc: lc, 1030 }, 1031 { 1032 opts: ObjectOpts{ 1033 Name: "folder1/folder1/exp_dt=9999-01-01/obj-1", 1034 ModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), 1035 VersionID: "", 1036 IsLatest: true, 1037 NumVersions: 1, 1038 }, 1039 want: 0, 1040 lc: lc, 1041 }, 1042 } 1043 for i, tc := range tests { 1044 t.Run(fmt.Sprintf("test-%d", i+1), func(t *testing.T) { 1045 if got := tc.lc.FilterRules(tc.opts); len(got) != tc.want { 1046 t.Fatalf("Expected %d rules to match but got %d", tc.want, len(got)) 1047 } 1048 w := httptest.NewRecorder() 1049 tc.lc.SetPredictionHeaders(w, tc.opts) 1050 expHdr, ok := w.Header()[xhttp.AmzExpiration] 1051 switch { 1052 case ok && tc.want == 0: 1053 t.Fatalf("Expected no rule to match but found x-amz-expiration header set: %v", expHdr) 1054 case !ok && tc.want > 0: 1055 t.Fatal("Expected x-amz-expiration header to be set but not found") 1056 } 1057 }) 1058 } 1059 } 1060 1061 func TestFilterRules(t *testing.T) { 1062 rules := []Rule{ 1063 { 1064 ID: "rule-1", 1065 Status: "Enabled", 1066 Filter: Filter{ 1067 set: true, 1068 Tag: Tag{ 1069 Key: "key1", 1070 Value: "val1", 1071 }, 1072 }, 1073 Expiration: Expiration{ 1074 set: true, 1075 Days: 1, 1076 }, 1077 }, 1078 { 1079 ID: "rule-with-sz-lt", 1080 Status: "Enabled", 1081 Filter: Filter{ 1082 set: true, 1083 ObjectSizeLessThan: 100 * humanize.MiByte, 1084 }, 1085 Expiration: Expiration{ 1086 set: true, 1087 Days: 1, 1088 }, 1089 }, 1090 { 1091 ID: "rule-with-sz-gt", 1092 Status: "Enabled", 1093 Filter: Filter{ 1094 set: true, 1095 ObjectSizeGreaterThan: 1 * humanize.MiByte, 1096 }, 1097 Expiration: Expiration{ 1098 set: true, 1099 Days: 1, 1100 }, 1101 }, 1102 { 1103 ID: "rule-with-sz-lt-and-tag", 1104 Status: "Enabled", 1105 Filter: Filter{ 1106 set: true, 1107 And: And{ 1108 ObjectSizeLessThan: 100 * humanize.MiByte, 1109 Tags: []Tag{ 1110 { 1111 Key: "key1", 1112 Value: "val1", 1113 }, 1114 }, 1115 }, 1116 }, 1117 Expiration: Expiration{ 1118 set: true, 1119 Days: 1, 1120 }, 1121 }, 1122 { 1123 ID: "rule-with-sz-gt-and-tag", 1124 Status: "Enabled", 1125 Filter: Filter{ 1126 set: true, 1127 And: And{ 1128 ObjectSizeGreaterThan: 1 * humanize.MiByte, 1129 Tags: []Tag{ 1130 { 1131 Key: "key1", 1132 Value: "val1", 1133 }, 1134 }, 1135 }, 1136 }, 1137 Expiration: Expiration{ 1138 set: true, 1139 Days: 1, 1140 }, 1141 }, 1142 { 1143 ID: "rule-with-sz-lt-and-gt", 1144 Status: "Enabled", 1145 Filter: Filter{ 1146 set: true, 1147 And: And{ 1148 ObjectSizeGreaterThan: 101 * humanize.MiByte, 1149 ObjectSizeLessThan: 200 * humanize.MiByte, 1150 }, 1151 }, 1152 Expiration: Expiration{ 1153 set: true, 1154 Days: 1, 1155 }, 1156 }, 1157 } 1158 tests := []struct { 1159 lc Lifecycle 1160 opts ObjectOpts 1161 hasRules bool 1162 }{ 1163 { // Delete marker should match filter without tags 1164 lc: Lifecycle{ 1165 Rules: []Rule{ 1166 rules[0], 1167 }, 1168 }, 1169 opts: ObjectOpts{ 1170 DeleteMarker: true, 1171 IsLatest: true, 1172 Name: "obj-1", 1173 }, 1174 hasRules: true, 1175 }, 1176 { // PUT version with no matching tags 1177 lc: Lifecycle{ 1178 Rules: []Rule{ 1179 rules[0], 1180 }, 1181 }, 1182 opts: ObjectOpts{ 1183 IsLatest: true, 1184 Name: "obj-1", 1185 Size: 1 * humanize.MiByte, 1186 }, 1187 hasRules: false, 1188 }, 1189 { // PUT version with matching tags 1190 lc: Lifecycle{ 1191 Rules: []Rule{ 1192 rules[0], 1193 }, 1194 }, 1195 opts: ObjectOpts{ 1196 IsLatest: true, 1197 UserTags: "key1=val1", 1198 Name: "obj-1", 1199 Size: 2 * humanize.MiByte, 1200 }, 1201 hasRules: true, 1202 }, 1203 { // PUT version with size based filters 1204 lc: Lifecycle{ 1205 Rules: []Rule{ 1206 rules[1], 1207 rules[2], 1208 rules[3], 1209 rules[4], 1210 rules[5], 1211 }, 1212 }, 1213 opts: ObjectOpts{ 1214 IsLatest: true, 1215 UserTags: "key1=val1", 1216 Name: "obj-1", 1217 Size: 1*humanize.MiByte - 1, 1218 }, 1219 hasRules: true, 1220 }, 1221 { // PUT version with size based filters 1222 lc: Lifecycle{ 1223 Rules: []Rule{ 1224 rules[1], 1225 rules[2], 1226 rules[3], 1227 rules[4], 1228 rules[5], 1229 }, 1230 }, 1231 opts: ObjectOpts{ 1232 IsLatest: true, 1233 Name: "obj-1", 1234 Size: 1*humanize.MiByte + 1, 1235 }, 1236 hasRules: true, 1237 }, 1238 { // DEL version with size based filters 1239 lc: Lifecycle{ 1240 Rules: []Rule{ 1241 rules[1], 1242 rules[2], 1243 rules[3], 1244 rules[4], 1245 rules[5], 1246 }, 1247 }, 1248 opts: ObjectOpts{ 1249 DeleteMarker: true, 1250 IsLatest: true, 1251 Name: "obj-1", 1252 }, 1253 hasRules: true, 1254 }, 1255 } 1256 1257 for i, tc := range tests { 1258 t.Run(fmt.Sprintf("test-%d", i+1), func(t *testing.T) { 1259 if err := tc.lc.Validate(); err != nil { 1260 t.Fatalf("Lifecycle validation failed - %v", err) 1261 } 1262 rules := tc.lc.FilterRules(tc.opts) 1263 if tc.hasRules && len(rules) == 0 { 1264 t.Fatalf("%d: Expected at least one rule to match but none matched", i+1) 1265 } 1266 if !tc.hasRules && len(rules) > 0 { 1267 t.Fatalf("%d: Expected no rules to match but got matches %v", i+1, rules) 1268 } 1269 }) 1270 } 1271 }