github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/bucket/replication/replication_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 replication
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"testing"
    24  )
    25  
    26  func TestParseAndValidateReplicationConfig(t *testing.T) {
    27  	testCases := []struct {
    28  		inputConfig           string
    29  		expectedParsingErr    error
    30  		expectedValidationErr error
    31  		destBucket            string
    32  		sameTarget            bool
    33  	}{
    34  		{ // 1 Invalid delete marker status in replication config
    35  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>string</Status></DeleteMarkerReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
    36  			destBucket:            "destinationbucket",
    37  			sameTarget:            false,
    38  			expectedParsingErr:    nil,
    39  			expectedValidationErr: errInvalidDeleteMarkerReplicationStatus,
    40  		},
    41  		// 2 No delete replication status in replication config
    42  		{
    43  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
    44  			destBucket:            "destinationbucket",
    45  			sameTarget:            false,
    46  			expectedParsingErr:    nil,
    47  			expectedValidationErr: nil,
    48  		},
    49  		// 3 valid replication config
    50  		{
    51  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
    52  			destBucket:            "destinationbucket",
    53  			sameTarget:            false,
    54  			expectedParsingErr:    nil,
    55  			expectedValidationErr: nil,
    56  		},
    57  		// 4 missing role in config and destination ARN is in legacy format
    58  		{
    59  			inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
    60  			// destination bucket in config different from bucket specified
    61  			destBucket:            "destinationbucket",
    62  			sameTarget:            false,
    63  			expectedParsingErr:    nil,
    64  			expectedValidationErr: errDestinationArnMissing,
    65  		},
    66  		// 5 replication destination in different rules not identical
    67  		{
    68  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role></Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:minio:replication:::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><Priority>3</Priority><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:minio:replication:::destinationbucket2</Bucket></Destination></Rule></ReplicationConfiguration>`,
    69  			destBucket:            "destinationbucket",
    70  			sameTarget:            false,
    71  			expectedParsingErr:    nil,
    72  			expectedValidationErr: nil,
    73  		},
    74  		// 6 missing rule status in replication config
    75  		{
    76  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
    77  			destBucket:            "destinationbucket",
    78  			sameTarget:            false,
    79  			expectedParsingErr:    nil,
    80  			expectedValidationErr: errEmptyRuleStatus,
    81  		},
    82  		// 7 invalid rule status in replication config
    83  		{
    84  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enssabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
    85  			destBucket:            "destinationbucket",
    86  			sameTarget:            false,
    87  			expectedParsingErr:    nil,
    88  			expectedValidationErr: errInvalidRuleStatus,
    89  		},
    90  		// 8 invalid rule id exceeds length allowed in replication config
    91  		{
    92  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><ID>vsUVERgOc8zZYagLSzSa5lE8qeI6nh1lyLNS4R9W052yfecrhhepGboswSWMMNO8CPcXM4GM3nKyQ72EadlMzzZBFoYWKn7ju5GoE5w9c57a0piHR1vexpdd9FrMquiruvAJ0MTGVupm0EegMVxoIOdjx7VgZhGrmi2XDvpVEFT7WmYMA9fSK297XkTHWyECaNHBySJ1Qp4vwX8tPNauKpfHx4kzUpnKe1PZbptGMWbY5qTcwlNuMhVSmgFffShq</ID><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
    93  			destBucket:            "destinationbucket",
    94  			sameTarget:            false,
    95  			expectedParsingErr:    nil,
    96  			expectedValidationErr: errInvalidRuleID,
    97  		},
    98  		// 9 invalid priority status in replication config
    99  		{
   100  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   101  			destBucket:            "destinationbucket",
   102  			sameTarget:            false,
   103  			expectedParsingErr:    nil,
   104  			expectedValidationErr: errReplicationUniquePriority,
   105  		},
   106  		// 10 no rule in replication config
   107  		{
   108  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role></ReplicationConfiguration>`,
   109  			destBucket:            "destinationbucket",
   110  			sameTarget:            false,
   111  			expectedParsingErr:    nil,
   112  			expectedValidationErr: errReplicationNoRule,
   113  		},
   114  		// 11 no destination in replication config
   115  		{
   116  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination></Destination></Rule></ReplicationConfiguration>`,
   117  			destBucket:            "destinationbucket",
   118  			sameTarget:            false,
   119  			expectedParsingErr:    Errorf("invalid destination '%v'", ""),
   120  			expectedValidationErr: nil,
   121  		},
   122  		// 12 destination not matching ARN in replication config
   123  		{
   124  			inputConfig:           `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>destinationbucket2</Bucket></Destination></Rule></ReplicationConfiguration>`,
   125  			destBucket:            "destinationbucket",
   126  			sameTarget:            false,
   127  			expectedParsingErr:    fmt.Errorf("invalid destination '%v'", "destinationbucket2"),
   128  			expectedValidationErr: nil,
   129  		},
   130  		// 13 missing role in config and destination ARN has target ARN
   131  		{
   132  			inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:minio:replication::8320b6d18f9032b4700f1f03b50d8d1853de8f22cab86931ee794e12f190852c:destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   133  			// destination bucket in config different from bucket specified
   134  			destBucket:            "destinationbucket",
   135  			sameTarget:            false,
   136  			expectedParsingErr:    nil,
   137  			expectedValidationErr: nil,
   138  		},
   139  		// 14 role absent in config and destination ARN has target ARN in invalid format
   140  		{
   141  			inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:xx:replication::8320b6d18f9032b4700f1f03b50d8d1853de8f22cab86931ee794e12f190852c:destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   142  			// destination bucket in config different from bucket specified
   143  			destBucket:            "destinationbucket",
   144  			sameTarget:            false,
   145  			expectedParsingErr:    fmt.Errorf("invalid destination '%v'", "arn:xx:replication::8320b6d18f9032b4700f1f03b50d8d1853de8f22cab86931ee794e12f190852c:destinationbucket"),
   146  			expectedValidationErr: nil,
   147  		},
   148  	}
   149  	for i, tc := range testCases {
   150  		t.Run(fmt.Sprintf("Test %d", i+1), func(t *testing.T) {
   151  			cfg, err := ParseConfig(bytes.NewReader([]byte(tc.inputConfig)))
   152  			if err != nil && tc.expectedParsingErr != nil && err.Error() != tc.expectedParsingErr.Error() {
   153  				t.Fatalf("%d: Expected '%v' during parsing but got '%v'", i+1, tc.expectedParsingErr, err)
   154  			}
   155  			if err == nil && tc.expectedParsingErr != nil {
   156  				t.Fatalf("%d: Expected '%v' during parsing but got '%v'", i+1, tc.expectedParsingErr, err)
   157  			}
   158  			if tc.expectedParsingErr != nil {
   159  				// We already expect a parsing error,
   160  				// no need to continue this test.
   161  				return
   162  			}
   163  			err = cfg.Validate(tc.destBucket, tc.sameTarget)
   164  			if err != tc.expectedValidationErr {
   165  				t.Fatalf("%d: Expected %v during parsing but got %v", i+1, tc.expectedValidationErr, err)
   166  			}
   167  		})
   168  	}
   169  }
   170  
   171  func TestReplicate(t *testing.T) {
   172  	cfgs := []Config{
   173  		{ // Config0 - Replication config has no filters, all replication enabled
   174  			Rules: []Rule{
   175  				{
   176  					Status:                  Enabled,
   177  					Priority:                3,
   178  					DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
   179  					DeleteReplication:       DeleteReplication{Status: Enabled},
   180  					Filter:                  Filter{},
   181  				},
   182  			},
   183  		},
   184  		{ // Config1 - Replication config has no filters, delete,delete-marker replication disabled
   185  			Rules: []Rule{
   186  				{
   187  					Status:                  Enabled,
   188  					Priority:                3,
   189  					DeleteMarkerReplication: DeleteMarkerReplication{Status: Disabled},
   190  					DeleteReplication:       DeleteReplication{Status: Disabled},
   191  					Filter:                  Filter{},
   192  				},
   193  			},
   194  		},
   195  		{ // Config2 - Replication config has filters and more than 1 matching rule, delete,delete-marker replication disabled
   196  			Rules: []Rule{
   197  				{
   198  					Status:                  Enabled,
   199  					Priority:                2,
   200  					DeleteMarkerReplication: DeleteMarkerReplication{Status: Disabled},
   201  					DeleteReplication:       DeleteReplication{Status: Enabled},
   202  					Filter:                  Filter{Prefix: "xy", And: And{}, Tag: Tag{Key: "k1", Value: "v1"}},
   203  				},
   204  				{
   205  					Status:                  Enabled,
   206  					Priority:                1,
   207  					DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
   208  					DeleteReplication:       DeleteReplication{Status: Disabled},
   209  					Filter:                  Filter{Prefix: "xyz"},
   210  				},
   211  			},
   212  		},
   213  		{ // Config3 - Replication config has filters and no overlapping rules
   214  			Rules: []Rule{
   215  				{
   216  					Status:                  Enabled,
   217  					Priority:                2,
   218  					DeleteMarkerReplication: DeleteMarkerReplication{Status: Disabled},
   219  					DeleteReplication:       DeleteReplication{Status: Enabled},
   220  					Filter:                  Filter{Prefix: "xy", And: And{}, Tag: Tag{Key: "k1", Value: "v1"}},
   221  				},
   222  				{
   223  					Status:                  Enabled,
   224  					Priority:                1,
   225  					DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
   226  					DeleteReplication:       DeleteReplication{Status: Disabled},
   227  					Filter:                  Filter{Prefix: "abc"},
   228  				},
   229  			},
   230  		},
   231  		{ // Config4 - Replication config has filters and SourceSelectionCriteria Disabled
   232  			Rules: []Rule{
   233  				{
   234  					Status:                  Enabled,
   235  					Priority:                2,
   236  					DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled},
   237  					DeleteReplication:       DeleteReplication{Status: Enabled},
   238  					SourceSelectionCriteria: SourceSelectionCriteria{ReplicaModifications: ReplicaModifications{Status: Disabled}},
   239  				},
   240  			},
   241  		},
   242  	}
   243  	testCases := []struct {
   244  		opts           ObjectOpts
   245  		c              Config
   246  		expectedResult bool
   247  	}{
   248  		// using config 1 - no filters, all replication enabled
   249  		{ObjectOpts{}, cfgs[0], false},                                // 1. invalid ObjectOpts missing object name
   250  		{ObjectOpts{Name: "c1test"}, cfgs[0], true},                   // 2. valid ObjectOpts passing empty Filter
   251  		{ObjectOpts{Name: "c1test", VersionID: "vid"}, cfgs[0], true}, // 3. valid ObjectOpts passing empty Filter
   252  
   253  		{ObjectOpts{Name: "c1test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[0], true},                               // 4. DeleteMarker version replication valid case - matches DeleteMarkerReplication status
   254  		{ObjectOpts{Name: "c1test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[0], true},                                 // 5. permanent delete of version, matches DeleteReplication status - valid case
   255  		{ObjectOpts{Name: "c1test", VersionID: "vid", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[0], true},             // 6. permanent delete of version, matches DeleteReplication status
   256  		{ObjectOpts{Name: "c1test", VersionID: "vid", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[0], true}, // 7. permanent delete of version
   257  		{ObjectOpts{Name: "c1test", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[0], true},                   // 8. setting DeleteMarker on SSE-C encrypted object
   258  		{ObjectOpts{Name: "c1test", SSEC: true}, cfgs[0], true},                                                                      // 9. replication of SSE-C encrypted object
   259  
   260  		//  using config 2 - no filters, only replication of object, metadata enabled
   261  		{ObjectOpts{Name: "c2test"}, cfgs[1], true},                                                                                   // 10. valid ObjectOpts passing empty Filter
   262  		{ObjectOpts{Name: "c2test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[1], false},                               // 11. DeleteMarker version replication  not allowed due to DeleteMarkerReplication status
   263  		{ObjectOpts{Name: "c2test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[1], false},                                 // 12. permanent delete of version, disallowed by DeleteReplication status
   264  		{ObjectOpts{Name: "c2test", VersionID: "vid", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[1], false},             // 13. permanent delete of DeleteMarker version, disallowed by DeleteReplication status
   265  		{ObjectOpts{Name: "c2test", VersionID: "vid", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[1], false}, // 14. permanent delete of version, disqualified by SSE-C & DeleteReplication status
   266  		{ObjectOpts{Name: "c2test", DeleteMarker: true, SSEC: true, OpType: DeleteReplicationType}, cfgs[1], false},                   // 15. setting DeleteMarker on SSE-C encrypted object, disqualified by SSE-C & DeleteMarkerReplication status
   267  		{ObjectOpts{Name: "c2test", SSEC: true}, cfgs[1], true},                                                                       // 16. replication of SSE-C encrypted object
   268  		// using config 2 - has more than one rule with overlapping prefixes
   269  		{ObjectOpts{Name: "xy/c3test", UserTags: "k1=v1"}, cfgs[2], true},                                                                       // 17. matches rule 1 for replication of content/metadata
   270  		{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1"}, cfgs[2], true},                                                                      // 18. matches rule 1 for replication of content/metadata
   271  		{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[2], false},                  // 19. matches rule 1 - DeleteMarker replication disallowed by rule
   272  		{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], true}, // 20. matches rule 1 - DeleteReplication allowed by rule for permanent delete of DeleteMarker
   273  		{ObjectOpts{Name: "xyz/c3test", UserTags: "k1=v1", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], true},                     // 21. matches rule 1 - DeleteReplication allowed by rule for permanent delete of version
   274  		{ObjectOpts{Name: "xyz/c3test"}, cfgs[2], true},                                                                                         // 22. matches rule 2 for replication of content/metadata
   275  		{ObjectOpts{Name: "xy/c3test", UserTags: "k1=v2"}, cfgs[2], false},                                                                      // 23. does not match rule1 because tag value does not pass filter
   276  		{ObjectOpts{Name: "xyz/c3test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[2], true},                                      // 24. matches rule 2 - DeleteMarker replication allowed by rule
   277  		{ObjectOpts{Name: "xyz/c3test", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], false},                   // 25. matches rule 2 - DeleteReplication disallowed by rule for permanent delete of DeleteMarker
   278  		{ObjectOpts{Name: "xyz/c3test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[2], false},                                       // 26. matches rule 1 - DeleteReplication disallowed by rule for permanent delete of version
   279  		{ObjectOpts{Name: "abc/c3test"}, cfgs[2], false},                                                                                        // 27. matches no rule because object prefix does not match
   280  
   281  		// using config 3 - has no overlapping rules
   282  		{ObjectOpts{Name: "xy/c4test", UserTags: "k1=v1"}, cfgs[3], true},                                                                       // 28. matches rule 1 for replication of content/metadata
   283  		{ObjectOpts{Name: "xa/c4test", UserTags: "k1=v1"}, cfgs[3], false},                                                                      // 29. no rule match object prefix not in rules
   284  		{ObjectOpts{Name: "xyz/c4test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[3], false},                                     // 30. rule 1 not matched because of tags filter
   285  		{ObjectOpts{Name: "xyz/c4test", UserTags: "k1=v1", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[3], false},                  // 31. matches rule 1 - DeleteMarker replication disallowed by rule
   286  		{ObjectOpts{Name: "xyz/c4test", UserTags: "k1=v1", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], true}, // 32. matches rule 1 - DeleteReplication allowed by rule for permanent delete of DeleteMarker
   287  		{ObjectOpts{Name: "xyz/c4test", UserTags: "k1=v1", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], true},                     // 33. matches rule 1 - DeleteReplication allowed by rule for permanent delete of version
   288  		{ObjectOpts{Name: "abc/c4test"}, cfgs[3], true},                                                                                         // 34. matches rule 2 for replication of content/metadata
   289  		{ObjectOpts{Name: "abc/c4test", UserTags: "k1=v2"}, cfgs[3], true},                                                                      // 35. matches rule 2 for replication of content/metadata
   290  		{ObjectOpts{Name: "abc/c4test", DeleteMarker: true, OpType: DeleteReplicationType}, cfgs[3], true},                                      // 36. matches rule 2 - DeleteMarker replication allowed by rule
   291  		{ObjectOpts{Name: "abc/c4test", DeleteMarker: true, VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], false},                   // 37. matches rule 2 - DeleteReplication disallowed by rule for permanent delete of DeleteMarker
   292  		{ObjectOpts{Name: "abc/c4test", VersionID: "vid", OpType: DeleteReplicationType}, cfgs[3], false},                                       // 38. matches rule 2 - DeleteReplication disallowed by rule for permanent delete of version
   293  		//  using config 4 - with replica modification sync disabled.
   294  		{ObjectOpts{Name: "xy/c5test", UserTags: "k1=v1", Replica: true}, cfgs[4], false}, // 39. replica syncing disabled, this object is a replica
   295  		{ObjectOpts{Name: "xa/c5test", UserTags: "k1=v1", Replica: false}, cfgs[4], true}, // 40. replica syncing disabled, this object is NOT a replica
   296  	}
   297  
   298  	for _, testCase := range testCases {
   299  		testCase := testCase
   300  		t.Run(testCase.opts.Name, func(t *testing.T) {
   301  			result := testCase.c.Replicate(testCase.opts)
   302  			if result != testCase.expectedResult {
   303  				t.Errorf("expected: %v, got: %v", testCase.expectedResult, result)
   304  			}
   305  		})
   306  	}
   307  }
   308  
   309  func TestHasActiveRules(t *testing.T) {
   310  	testCases := []struct {
   311  		inputConfig    string
   312  		prefix         string
   313  		expectedNonRec bool
   314  		expectedRec    bool
   315  	}{
   316  		// case 1 - only one rule which is in Disabled status
   317  		{
   318  			inputConfig:    `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Disabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>key-prefix</Prefix><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   319  			prefix:         "miss/prefix",
   320  			expectedNonRec: false,
   321  			expectedRec:    false,
   322  		},
   323  		// case 2 - only one rule which matches prefix filter
   324  		{
   325  			inputConfig:    `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Filter><Prefix>key/prefix</Prefix></Filter><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   326  			prefix:         "key/prefix1",
   327  			expectedNonRec: true,
   328  			expectedRec:    true,
   329  		},
   330  		// case 3 - empty prefix
   331  		{
   332  			inputConfig:    `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   333  			prefix:         "key-prefix",
   334  			expectedNonRec: true,
   335  			expectedRec:    true,
   336  		},
   337  		// case 4 - has Filter based on prefix
   338  		{
   339  			inputConfig:    `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Filter><Prefix>testdir/dir1/</Prefix></Filter><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   340  			prefix:         "testdir/",
   341  			expectedNonRec: false,
   342  			expectedRec:    true,
   343  		},
   344  		// case 5 - has filter with prefix and tags, here we are not matching on tags
   345  		{
   346  			inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Filter>
   347  		<And><Prefix>key-prefix</Prefix><Tag><Key>key1</Key><Value>value1</Value></Tag><Tag><Key>key2</Key><Value>value2</Value></Tag></And></Filter><Destination><Bucket>arn:aws:s3:::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   348  			prefix:         "testdir/",
   349  			expectedNonRec: true,
   350  			expectedRec:    true,
   351  		},
   352  	}
   353  
   354  	for i, tc := range testCases {
   355  		tc := tc
   356  		t.Run(fmt.Sprintf("Test_%d", i+1), func(t *testing.T) {
   357  			cfg, err := ParseConfig(bytes.NewReader([]byte(tc.inputConfig)))
   358  			if err != nil {
   359  				t.Fatalf("Got unexpected error: %v", err)
   360  			}
   361  			if got := cfg.HasActiveRules(tc.prefix, false); got != tc.expectedNonRec {
   362  				t.Fatalf("Expected result with recursive set to false: `%v`, got: `%v`", tc.expectedNonRec, got)
   363  			}
   364  			if got := cfg.HasActiveRules(tc.prefix, true); got != tc.expectedRec {
   365  				t.Fatalf("Expected result with recursive set to true: `%v`, got: `%v`", tc.expectedRec, got)
   366  			}
   367  		})
   368  
   369  	}
   370  }
   371  
   372  func TestFilterActionableRules(t *testing.T) {
   373  	testCases := []struct {
   374  		inputConfig   string
   375  		prefix        string
   376  		ExpectedRules []Rule
   377  	}{
   378  		// case 1 - only one rule
   379  		{
   380  			inputConfig:   `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>prefix</Prefix><Priority>1</Priority><Destination><Bucket>arn:minio:replication:xxx::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   381  			prefix:        "prefix",
   382  			ExpectedRules: []Rule{{Status: Enabled, Priority: 1, DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled}, DeleteReplication: DeleteReplication{Status: Disabled}, Destination: Destination{Bucket: "destinationbucket", ARN: "arn:minio:replication:xxx::destinationbucket"}}},
   383  		},
   384  		// case 2 - multiple rules for same target, overlapping rules with different priority
   385  		{
   386  			inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>prefix</Prefix><Priority>3</Priority><Destination><Bucket>arn:minio:replication:xxx::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>prefix</Prefix><Priority>1</Priority><Destination><Bucket>arn:minio:replication:xxx::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   387  			prefix:      "prefix",
   388  			ExpectedRules: []Rule{
   389  				{Status: Enabled, Priority: 3, DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled}, DeleteReplication: DeleteReplication{Status: Disabled}, Destination: Destination{Bucket: "destinationbucket", ARN: "arn:minio:replication:xxx::destinationbucket"}},
   390  				{Status: Enabled, Priority: 1, DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled}, DeleteReplication: DeleteReplication{Status: Disabled}, Destination: Destination{Bucket: "destinationbucket", ARN: "arn:minio:replication:xxx::destinationbucket"}},
   391  			},
   392  		},
   393  		// case 3 - multiple rules for different target, overlapping rules on a target
   394  		{
   395  			inputConfig: `<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::AcctID:role/role-name</Role><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>prefix</Prefix><Priority>2</Priority><Destination><Bucket>arn:minio:replication:xxx::destinationbucket2</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>prefix</Prefix><Priority>4</Priority><Destination><Bucket>arn:minio:replication:xxx::destinationbucket2</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>prefix</Prefix><Priority>3</Priority><Destination><Bucket>arn:minio:replication:xxx::destinationbucket</Bucket></Destination></Rule><Rule><Status>Enabled</Status><DeleteMarkerReplication><Status>Disabled</Status></DeleteMarkerReplication><DeleteReplication><Status>Disabled</Status></DeleteReplication><Prefix>prefix</Prefix><Priority>1</Priority><Destination><Bucket>arn:minio:replication:xxx::destinationbucket</Bucket></Destination></Rule></ReplicationConfiguration>`,
   396  			prefix:      "prefix",
   397  			ExpectedRules: []Rule{
   398  				{Status: Enabled, Priority: 4, DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled}, DeleteReplication: DeleteReplication{Status: Disabled}, Destination: Destination{Bucket: "destinationbucket2", ARN: "arn:minio:replication:xxx::destinationbucket2"}},
   399  				{Status: Enabled, Priority: 2, DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled}, DeleteReplication: DeleteReplication{Status: Disabled}, Destination: Destination{Bucket: "destinationbucket2", ARN: "arn:minio:replication:xxx::destinationbucket2"}},
   400  				{Status: Enabled, Priority: 3, DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled}, DeleteReplication: DeleteReplication{Status: Disabled}, Destination: Destination{Bucket: "destinationbucket", ARN: "arn:minio:replication:xxx::destinationbucket"}},
   401  				{Status: Enabled, Priority: 1, DeleteMarkerReplication: DeleteMarkerReplication{Status: Enabled}, DeleteReplication: DeleteReplication{Status: Disabled}, Destination: Destination{Bucket: "destinationbucket", ARN: "arn:minio:replication:xxx::destinationbucket"}},
   402  			},
   403  		},
   404  	}
   405  	for _, tc := range testCases {
   406  		tc := tc
   407  		cfg, err := ParseConfig(bytes.NewReader([]byte(tc.inputConfig)))
   408  		if err != nil {
   409  			t.Fatalf("Got unexpected error: %v", err)
   410  		}
   411  		got := cfg.FilterActionableRules(ObjectOpts{Name: tc.prefix})
   412  		if len(got) != len(tc.ExpectedRules) {
   413  			t.Fatalf("Expected matching number of actionable rules: `%v`, got: `%v`", tc.ExpectedRules, got)
   414  		}
   415  		for i := range got {
   416  			if got[i].Destination.ARN != tc.ExpectedRules[i].Destination.ARN || got[i].Priority != tc.ExpectedRules[i].Priority {
   417  				t.Fatalf("Expected order of filtered rules to be identical: `%v`, got: `%v`", tc.ExpectedRules, got)
   418  			}
   419  		}
   420  	}
   421  }