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  }