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  }