storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/bucket/lifecycle/lifecycle_test.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2019 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package lifecycle 18 19 import ( 20 "bytes" 21 "encoding/xml" 22 "fmt" 23 "testing" 24 "time" 25 ) 26 27 func TestParseAndValidateLifecycleConfig(t *testing.T) { 28 testCases := []struct { 29 inputConfig string 30 expectedParsingErr error 31 expectedValidationErr error 32 }{ 33 { // Valid lifecycle config 34 inputConfig: `<LifecycleConfiguration> 35 <Rule> 36 <ID>testRule1</ID> 37 <Filter> 38 <Prefix>prefix</Prefix> 39 </Filter> 40 <Status>Enabled</Status> 41 <Expiration><Days>3</Days></Expiration> 42 </Rule> 43 <Rule> 44 <ID>testRule2</ID> 45 <Filter> 46 <Prefix>another-prefix</Prefix> 47 </Filter> 48 <Status>Enabled</Status> 49 <Expiration><Days>3</Days></Expiration> 50 </Rule> 51 </LifecycleConfiguration>`, 52 expectedParsingErr: nil, 53 expectedValidationErr: nil, 54 }, 55 { // Valid lifecycle config 56 inputConfig: `<LifecycleConfiguration> 57 <Rule> 58 <Filter> 59 <And><Tag><Key>key1</Key><Value>val1</Value><Key>key2</Key><Value>val2</Value></Tag></And> 60 </Filter> 61 <Expiration><Days>3</Days></Expiration> 62 </Rule> 63 </LifecycleConfiguration>`, 64 expectedParsingErr: errDuplicatedXMLTag, 65 expectedValidationErr: nil, 66 }, 67 { // lifecycle config with no rules 68 inputConfig: `<LifecycleConfiguration> 69 </LifecycleConfiguration>`, 70 expectedParsingErr: nil, 71 expectedValidationErr: errLifecycleNoRule, 72 }, 73 { // lifecycle config with rules having overlapping prefix 74 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> `, 75 expectedParsingErr: nil, 76 expectedValidationErr: nil, 77 }, 78 { // lifecycle config with rules having duplicate ID 79 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>`, 80 expectedParsingErr: nil, 81 expectedValidationErr: errLifecycleDuplicateID, 82 }, 83 // Missing <Tag> in <And> 84 { 85 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>`, 86 expectedParsingErr: nil, 87 expectedValidationErr: errXMLNotWellFormed, 88 }, 89 // Lifecycle with the deprecated Prefix tag 90 { 91 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>`, 92 expectedParsingErr: nil, 93 expectedValidationErr: nil, 94 }, 95 // Lifecycle with empty Filter tag 96 { 97 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>`, 98 expectedParsingErr: nil, 99 expectedValidationErr: nil, 100 }, 101 } 102 103 for i, tc := range testCases { 104 t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) { 105 lc, err := ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))) 106 if err != tc.expectedParsingErr { 107 t.Fatalf("%d: Expected %v during parsing but got %v", i+1, tc.expectedParsingErr, err) 108 } 109 if tc.expectedParsingErr != nil { 110 // We already expect a parsing error, 111 // no need to continue this test. 112 return 113 } 114 err = lc.Validate() 115 if err != tc.expectedValidationErr { 116 t.Fatalf("%d: Expected %v during parsing but got %v", i+1, tc.expectedValidationErr, err) 117 } 118 }) 119 } 120 } 121 122 // TestMarshalLifecycleConfig checks if lifecycleconfig xml 123 // marshaling/unmarshaling can handle output from each other 124 func TestMarshalLifecycleConfig(t *testing.T) { 125 // Time at midnight UTC 126 midnightTS := ExpirationDate{time.Date(2019, time.April, 20, 0, 0, 0, 0, time.UTC)} 127 lc := Lifecycle{ 128 Rules: []Rule{ 129 { 130 Status: "Enabled", 131 Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}}, 132 Expiration: Expiration{Days: ExpirationDays(3)}, 133 }, 134 { 135 Status: "Enabled", 136 Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}}, 137 Expiration: Expiration{Date: ExpirationDate(midnightTS)}, 138 }, 139 { 140 Status: "Enabled", 141 Filter: Filter{Prefix: Prefix{string: "prefix-1", set: true}}, 142 Expiration: Expiration{Date: ExpirationDate(midnightTS)}, 143 NoncurrentVersionTransition: NoncurrentVersionTransition{NoncurrentDays: 2, StorageClass: "TEST"}, 144 }, 145 }, 146 } 147 b, err := xml.MarshalIndent(&lc, "", "\t") 148 if err != nil { 149 t.Fatal(err) 150 } 151 var lc1 Lifecycle 152 err = xml.Unmarshal(b, &lc1) 153 if err != nil { 154 t.Fatal(err) 155 } 156 157 ruleSet := make(map[string]struct{}) 158 for _, rule := range lc.Rules { 159 ruleBytes, err := xml.Marshal(rule) 160 if err != nil { 161 t.Fatal(err) 162 } 163 ruleSet[string(ruleBytes)] = struct{}{} 164 } 165 for _, rule := range lc1.Rules { 166 ruleBytes, err := xml.Marshal(rule) 167 if err != nil { 168 t.Fatal(err) 169 } 170 if _, ok := ruleSet[string(ruleBytes)]; !ok { 171 t.Fatalf("Expected %v to be equal to %v, %v missing", lc, lc1, rule) 172 } 173 } 174 } 175 176 func TestExpectedExpiryTime(t *testing.T) { 177 testCases := []struct { 178 modTime time.Time 179 days ExpirationDays 180 expected time.Time 181 }{ 182 { 183 time.Date(2020, time.March, 15, 10, 10, 10, 0, time.UTC), 184 4, 185 time.Date(2020, time.March, 20, 0, 0, 0, 0, time.UTC), 186 }, 187 { 188 time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC), 189 1, 190 time.Date(2020, time.March, 17, 0, 0, 0, 0, time.UTC), 191 }, 192 } 193 194 for i, tc := range testCases { 195 t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) { 196 got := ExpectedExpiryTime(tc.modTime, int(tc.days)) 197 if !got.Equal(tc.expected) { 198 t.Fatalf("Expected %v to be equal to %v", got, tc.expected) 199 } 200 }) 201 } 202 203 } 204 205 func TestComputeActions(t *testing.T) { 206 testCases := []struct { 207 inputConfig string 208 objectName string 209 objectTags string 210 objectModTime time.Time 211 expectedAction Action 212 }{ 213 // Empty object name (unexpected case) should always return NoneAction 214 { 215 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>prefix</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 216 expectedAction: NoneAction, 217 }, 218 // Disabled should always return NoneAction 219 { 220 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Disabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 221 objectName: "foodir/fooobject", 222 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 223 expectedAction: NoneAction, 224 }, 225 // No modTime, should be none-action 226 { 227 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 228 objectName: "foodir/fooobject", 229 expectedAction: NoneAction, 230 }, 231 // Prefix not matched 232 { 233 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 234 objectName: "foxdir/fooobject", 235 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 236 expectedAction: NoneAction, 237 }, 238 // Test rule with empty prefix e.g. for whole bucket 239 { 240 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix></Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 241 objectName: "foxdir/fooobject/foo.txt", 242 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 243 expectedAction: DeleteAction, 244 }, 245 // Too early to remove (test Days) 246 { 247 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 248 objectName: "foxdir/fooobject", 249 objectModTime: time.Now().UTC().Add(-10 * 24 * time.Hour), // Created 10 days ago 250 expectedAction: NoneAction, 251 }, 252 // Should remove (test Days) 253 { 254 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 255 objectName: "foodir/fooobject", 256 objectModTime: time.Now().UTC().Add(-6 * 24 * time.Hour), // Created 6 days ago 257 expectedAction: DeleteAction, 258 }, 259 // Too early to remove (test Date) 260 { 261 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>`, 262 objectName: "foodir/fooobject", 263 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 264 expectedAction: NoneAction, 265 }, 266 // Should remove (test Days) 267 { 268 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>`, 269 objectName: "foodir/fooobject", 270 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 271 expectedAction: DeleteAction, 272 }, 273 // Should remove (Tags match) 274 { 275 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>`, 276 objectName: "foodir/fooobject", 277 objectTags: "tag1=value1&tag2=value2", 278 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 279 expectedAction: DeleteAction, 280 }, 281 // Should remove (Multiple Rules, Tags match) 282 { 283 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>`, 284 objectName: "foodir/fooobject", 285 objectTags: "tag1=value1&tag2=value2", 286 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 287 expectedAction: DeleteAction, 288 }, 289 // Should remove (Tags match) 290 { 291 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>`, 292 objectName: "foodir/fooobject", 293 objectTags: "tag1=value1&tag2=value2", 294 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 295 expectedAction: DeleteAction, 296 }, 297 // Should remove (Tags match with inverted order) 298 { 299 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>`, 300 objectName: "fooobject", 301 objectTags: "storeforever=false&factory=true", 302 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 303 expectedAction: DeleteAction, 304 }, 305 306 // Should not remove (Tags don't match) 307 { 308 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>`, 309 objectName: "foodir/fooobject", 310 objectTags: "tag1=value1", 311 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 312 expectedAction: NoneAction, 313 }, 314 // Should not remove (Tags match, but prefix doesn't match) 315 { 316 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>`, 317 objectName: "foxdir/fooobject", 318 objectTags: "tag1=value1", 319 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 320 expectedAction: NoneAction, 321 }, 322 // Should remove - empty prefix, tags match, date expiration kicked in 323 { 324 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>`, 325 objectName: "foxdir/fooobject", 326 objectTags: "tag1=value1", 327 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 328 expectedAction: DeleteAction, 329 }, 330 // Should remove - empty prefix, tags match, object is expired based on specified Days 331 { 332 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>`, 333 objectName: "foxdir/fooobject", 334 objectTags: "tag1=value1", 335 objectModTime: time.Now().UTC().Add(-48 * time.Hour), // Created 2 day ago 336 expectedAction: DeleteAction, 337 }, 338 // Should remove, the second rule has expiration kicked in 339 { 340 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>`, 341 objectName: "foxdir/fooobject", 342 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 343 expectedAction: DeleteAction, 344 }, 345 // Should accept BucketLifecycleConfiguration root tag 346 { 347 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>`, 348 objectName: "foodir/fooobject", 349 objectModTime: time.Now().UTC().Add(-24 * time.Hour), // Created 1 day ago 350 expectedAction: DeleteAction, 351 }, 352 } 353 354 for _, tc := range testCases { 355 tc := tc 356 t.Run("", func(t *testing.T) { 357 lc, err := ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))) 358 if err != nil { 359 t.Fatalf("Got unexpected error: %v", err) 360 } 361 if resultAction := lc.ComputeAction(ObjectOpts{ 362 Name: tc.objectName, 363 UserTags: tc.objectTags, 364 ModTime: tc.objectModTime, 365 IsLatest: true, 366 }); resultAction != tc.expectedAction { 367 t.Fatalf("Expected action: `%v`, got: `%v`", tc.expectedAction, resultAction) 368 } 369 }) 370 371 } 372 } 373 374 func TestHasActiveRules(t *testing.T) { 375 testCases := []struct { 376 inputConfig string 377 prefix string 378 expectedNonRec bool 379 expectedRec bool 380 }{ 381 { 382 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 383 prefix: "foodir/foobject", 384 expectedNonRec: true, expectedRec: true, 385 }, 386 { // empty prefix 387 inputConfig: `<LifecycleConfiguration><Rule><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 388 prefix: "foodir/foobject/foo.txt", 389 expectedNonRec: true, expectedRec: true, 390 }, 391 { 392 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 393 prefix: "zdir/foobject", 394 expectedNonRec: false, expectedRec: false, 395 }, 396 { 397 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/zdir/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 398 prefix: "foodir/", 399 expectedNonRec: false, expectedRec: true, 400 }, 401 { 402 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix></Prefix></Filter><Status>Disabled</Status><Expiration><Days>5</Days></Expiration></Rule></LifecycleConfiguration>`, 403 prefix: "foodir/", 404 expectedNonRec: false, expectedRec: false, 405 }, 406 { 407 inputConfig: `<LifecycleConfiguration><Rule><Filter><Prefix>foodir/</Prefix></Filter><Status>Enabled</Status><Expiration><Date>2999-01-01T00:00:00.000Z</Date></Expiration></Rule></LifecycleConfiguration>`, 408 prefix: "foodir/foobject", 409 expectedNonRec: false, expectedRec: false, 410 }, 411 } 412 413 for i, tc := range testCases { 414 tc := tc 415 t.Run(fmt.Sprintf("Test_%d", i+1), func(t *testing.T) { 416 lc, err := ParseLifecycleConfig(bytes.NewReader([]byte(tc.inputConfig))) 417 if err != nil { 418 t.Fatalf("Got unexpected error: %v", err) 419 } 420 if got := lc.HasActiveRules(tc.prefix, false); got != tc.expectedNonRec { 421 t.Fatalf("Expected result with recursive set to false: `%v`, got: `%v`", tc.expectedNonRec, got) 422 } 423 if got := lc.HasActiveRules(tc.prefix, true); got != tc.expectedRec { 424 t.Fatalf("Expected result with recursive set to true: `%v`, got: `%v`", tc.expectedRec, got) 425 } 426 427 }) 428 429 } 430 }