diff options
Diffstat (limited to 'weed/s3api/s3api_object_retention_test.go')
| -rw-r--r-- | weed/s3api/s3api_object_retention_test.go | 726 |
1 files changed, 726 insertions, 0 deletions
diff --git a/weed/s3api/s3api_object_retention_test.go b/weed/s3api/s3api_object_retention_test.go new file mode 100644 index 000000000..0caa50b42 --- /dev/null +++ b/weed/s3api/s3api_object_retention_test.go @@ -0,0 +1,726 @@ +package s3api + +import ( + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" +) + +// TODO: If needed, re-implement TestPutObjectRetention with proper setup for buckets, objects, and versioning. + +func TestValidateRetention(t *testing.T) { + tests := []struct { + name string + retention *ObjectRetention + expectError bool + errorMsg string + }{ + { + name: "Valid GOVERNANCE retention", + retention: &ObjectRetention{ + Mode: s3_constants.RetentionModeGovernance, + RetainUntilDate: timePtr(time.Now().Add(24 * time.Hour)), + }, + expectError: false, + }, + { + name: "Valid COMPLIANCE retention", + retention: &ObjectRetention{ + Mode: s3_constants.RetentionModeCompliance, + RetainUntilDate: timePtr(time.Now().Add(24 * time.Hour)), + }, + expectError: false, + }, + { + name: "Missing Mode", + retention: &ObjectRetention{ + RetainUntilDate: timePtr(time.Now().Add(24 * time.Hour)), + }, + expectError: true, + errorMsg: "retention configuration must specify Mode", + }, + { + name: "Missing RetainUntilDate", + retention: &ObjectRetention{ + Mode: s3_constants.RetentionModeGovernance, + }, + expectError: true, + errorMsg: "retention configuration must specify RetainUntilDate", + }, + { + name: "Invalid Mode", + retention: &ObjectRetention{ + Mode: "INVALID_MODE", + RetainUntilDate: timePtr(time.Now().Add(24 * time.Hour)), + }, + expectError: true, + errorMsg: "invalid retention mode", + }, + { + name: "Past RetainUntilDate", + retention: &ObjectRetention{ + Mode: s3_constants.RetentionModeGovernance, + RetainUntilDate: timePtr(time.Now().Add(-24 * time.Hour)), + }, + expectError: true, + errorMsg: "retain until date must be in the future", + }, + { + name: "Empty retention", + retention: &ObjectRetention{}, + expectError: true, + errorMsg: "retention configuration must specify Mode", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateRetention(tt.retention) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error message to contain '%s', got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestValidateLegalHold(t *testing.T) { + tests := []struct { + name string + legalHold *ObjectLegalHold + expectError bool + errorMsg string + }{ + { + name: "Valid ON status", + legalHold: &ObjectLegalHold{ + Status: s3_constants.LegalHoldOn, + }, + expectError: false, + }, + { + name: "Valid OFF status", + legalHold: &ObjectLegalHold{ + Status: s3_constants.LegalHoldOff, + }, + expectError: false, + }, + { + name: "Invalid status", + legalHold: &ObjectLegalHold{ + Status: "INVALID_STATUS", + }, + expectError: true, + errorMsg: "invalid legal hold status", + }, + { + name: "Empty status", + legalHold: &ObjectLegalHold{ + Status: "", + }, + expectError: true, + errorMsg: "invalid legal hold status", + }, + { + name: "Lowercase on", + legalHold: &ObjectLegalHold{ + Status: "on", + }, + expectError: true, + errorMsg: "invalid legal hold status", + }, + { + name: "Lowercase off", + legalHold: &ObjectLegalHold{ + Status: "off", + }, + expectError: true, + errorMsg: "invalid legal hold status", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateLegalHold(tt.legalHold) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error message to contain '%s', got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestParseObjectRetention(t *testing.T) { + tests := []struct { + name string + xmlBody string + expectError bool + errorMsg string + expectedResult *ObjectRetention + }{ + { + name: "Valid retention XML", + xmlBody: `<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <Mode>GOVERNANCE</Mode> + <RetainUntilDate>2024-12-31T23:59:59Z</RetainUntilDate> + </Retention>`, + expectError: false, + expectedResult: &ObjectRetention{ + Mode: "GOVERNANCE", + RetainUntilDate: timePtr(time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)), + }, + }, + { + name: "Valid compliance retention XML", + xmlBody: `<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <Mode>COMPLIANCE</Mode> + <RetainUntilDate>2025-01-01T00:00:00Z</RetainUntilDate> + </Retention>`, + expectError: false, + expectedResult: &ObjectRetention{ + Mode: "COMPLIANCE", + RetainUntilDate: timePtr(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + }, + { + name: "Empty XML body", + xmlBody: "", + expectError: true, + errorMsg: "error parsing XML", + }, + { + name: "Invalid XML", + xmlBody: `<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Mode>GOVERNANCE</Mode><RetainUntilDate>invalid-date</RetainUntilDate></Retention>`, + expectError: true, + errorMsg: "cannot parse", + }, + { + name: "Malformed XML", + xmlBody: "<Retention><Mode>GOVERNANCE</Mode><RetainUntilDate>2024-12-31T23:59:59Z</Retention>", + expectError: true, + errorMsg: "error parsing XML", + }, + { + name: "Missing Mode", + xmlBody: `<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <RetainUntilDate>2024-12-31T23:59:59Z</RetainUntilDate> + </Retention>`, + expectError: false, + expectedResult: &ObjectRetention{ + Mode: "", + RetainUntilDate: timePtr(time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)), + }, + }, + { + name: "Missing RetainUntilDate", + xmlBody: `<Retention xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <Mode>GOVERNANCE</Mode> + </Retention>`, + expectError: false, + expectedResult: &ObjectRetention{ + Mode: "GOVERNANCE", + RetainUntilDate: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock HTTP request with XML body + req := &http.Request{ + Body: io.NopCloser(strings.NewReader(tt.xmlBody)), + } + + result, err := parseObjectRetention(req) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error message to contain '%s', got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result == nil { + t.Errorf("Expected result but got nil") + } else { + if result.Mode != tt.expectedResult.Mode { + t.Errorf("Expected Mode %s, got %s", tt.expectedResult.Mode, result.Mode) + } + if tt.expectedResult.RetainUntilDate == nil { + if result.RetainUntilDate != nil { + t.Errorf("Expected RetainUntilDate to be nil, got %v", result.RetainUntilDate) + } + } else if result.RetainUntilDate == nil { + t.Errorf("Expected RetainUntilDate to be %v, got nil", tt.expectedResult.RetainUntilDate) + } else if !result.RetainUntilDate.Equal(*tt.expectedResult.RetainUntilDate) { + t.Errorf("Expected RetainUntilDate %v, got %v", tt.expectedResult.RetainUntilDate, result.RetainUntilDate) + } + } + } + }) + } +} + +func TestParseObjectLegalHold(t *testing.T) { + tests := []struct { + name string + xmlBody string + expectError bool + errorMsg string + expectedResult *ObjectLegalHold + }{ + { + name: "Valid legal hold ON", + xmlBody: `<LegalHold xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <Status>ON</Status> + </LegalHold>`, + expectError: false, + expectedResult: &ObjectLegalHold{ + Status: "ON", + }, + }, + { + name: "Valid legal hold OFF", + xmlBody: `<LegalHold xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <Status>OFF</Status> + </LegalHold>`, + expectError: false, + expectedResult: &ObjectLegalHold{ + Status: "OFF", + }, + }, + { + name: "Empty XML body", + xmlBody: "", + expectError: true, + errorMsg: "error parsing XML", + }, + { + name: "Invalid XML", + xmlBody: "<LegalHold><Status>ON</Status>", + expectError: true, + errorMsg: "error parsing XML", + }, + { + name: "Missing Status", + xmlBody: `<LegalHold xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + </LegalHold>`, + expectError: false, + expectedResult: &ObjectLegalHold{ + Status: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock HTTP request with XML body + req := &http.Request{ + Body: io.NopCloser(strings.NewReader(tt.xmlBody)), + } + + result, err := parseObjectLegalHold(req) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error message to contain '%s', got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result == nil { + t.Errorf("Expected result but got nil") + } else { + if result.Status != tt.expectedResult.Status { + t.Errorf("Expected Status %s, got %s", tt.expectedResult.Status, result.Status) + } + } + } + }) + } +} + +func TestParseObjectLockConfiguration(t *testing.T) { + tests := []struct { + name string + xmlBody string + expectError bool + errorMsg string + expectedResult *ObjectLockConfiguration + }{ + { + name: "Valid object lock configuration", + xmlBody: `<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <ObjectLockEnabled>Enabled</ObjectLockEnabled> + </ObjectLockConfiguration>`, + expectError: false, + expectedResult: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + }, + }, + { + name: "Valid object lock configuration with rule", + xmlBody: `<ObjectLockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <ObjectLockEnabled>Enabled</ObjectLockEnabled> + <Rule> + <DefaultRetention> + <Mode>GOVERNANCE</Mode> + <Days>30</Days> + </DefaultRetention> + </Rule> + </ObjectLockConfiguration>`, + expectError: false, + expectedResult: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "GOVERNANCE", + Days: 30, + }, + }, + }, + }, + { + name: "Empty XML body", + xmlBody: "", + expectError: true, + errorMsg: "error parsing XML", + }, + { + name: "Invalid XML", + xmlBody: "<ObjectLockConfiguration><ObjectLockEnabled>Enabled</ObjectLockEnabled>", + expectError: true, + errorMsg: "error parsing XML", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock HTTP request with XML body + req := &http.Request{ + Body: io.NopCloser(strings.NewReader(tt.xmlBody)), + } + + result, err := parseObjectLockConfiguration(req) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error message to contain '%s', got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result == nil { + t.Errorf("Expected result but got nil") + } else { + if result.ObjectLockEnabled != tt.expectedResult.ObjectLockEnabled { + t.Errorf("Expected ObjectLockEnabled %s, got %s", tt.expectedResult.ObjectLockEnabled, result.ObjectLockEnabled) + } + if tt.expectedResult.Rule == nil { + if result.Rule != nil { + t.Errorf("Expected Rule to be nil, got %v", result.Rule) + } + } else if result.Rule == nil { + t.Errorf("Expected Rule to be non-nil") + } else { + if result.Rule.DefaultRetention == nil { + t.Errorf("Expected DefaultRetention to be non-nil") + } else { + if result.Rule.DefaultRetention.Mode != tt.expectedResult.Rule.DefaultRetention.Mode { + t.Errorf("Expected DefaultRetention Mode %s, got %s", tt.expectedResult.Rule.DefaultRetention.Mode, result.Rule.DefaultRetention.Mode) + } + if result.Rule.DefaultRetention.Days != tt.expectedResult.Rule.DefaultRetention.Days { + t.Errorf("Expected DefaultRetention Days %d, got %d", tt.expectedResult.Rule.DefaultRetention.Days, result.Rule.DefaultRetention.Days) + } + } + } + } + } + }) + } +} + +func TestValidateObjectLockConfiguration(t *testing.T) { + tests := []struct { + name string + config *ObjectLockConfiguration + expectError bool + errorMsg string + }{ + { + name: "Valid config with ObjectLockEnabled only", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + }, + expectError: false, + }, + { + name: "Missing ObjectLockEnabled", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "", + }, + expectError: true, + errorMsg: "object lock configuration must specify ObjectLockEnabled", + }, + { + name: "Valid config with rule and days", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "GOVERNANCE", + Days: 30, + }, + }, + }, + expectError: false, + }, + { + name: "Valid config with rule and years", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "COMPLIANCE", + Years: 1, + }, + }, + }, + expectError: false, + }, + { + name: "Invalid ObjectLockEnabled value", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "InvalidValue", + }, + expectError: true, + errorMsg: "invalid object lock enabled value", + }, + { + name: "Invalid rule - missing mode", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Days: 30, + }, + }, + }, + expectError: true, + errorMsg: "default retention must specify Mode", + }, + { + name: "Invalid rule - both days and years", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "GOVERNANCE", + Days: 30, + Years: 1, + }, + }, + }, + expectError: true, + errorMsg: "default retention cannot specify both Days and Years", + }, + { + name: "Invalid rule - neither days nor years", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "GOVERNANCE", + }, + }, + }, + expectError: true, + errorMsg: "default retention must specify either Days or Years", + }, + { + name: "Invalid rule - invalid mode", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "INVALID_MODE", + Days: 30, + }, + }, + }, + expectError: true, + errorMsg: "invalid default retention mode", + }, + { + name: "Invalid rule - days out of range", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "GOVERNANCE", + Days: 50000, + }, + }, + }, + expectError: true, + errorMsg: fmt.Sprintf("default retention days must be between 0 and %d", MaxRetentionDays), + }, + { + name: "Invalid rule - years out of range", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: &DefaultRetention{ + Mode: "GOVERNANCE", + Years: 200, + }, + }, + }, + expectError: true, + errorMsg: fmt.Sprintf("default retention years must be between 0 and %d", MaxRetentionYears), + }, + { + name: "Invalid rule - missing DefaultRetention", + config: &ObjectLockConfiguration{ + ObjectLockEnabled: "Enabled", + Rule: &ObjectLockRule{ + DefaultRetention: nil, + }, + }, + expectError: true, + errorMsg: "rule configuration must specify DefaultRetention", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateObjectLockConfiguration(tt.config) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error message to contain '%s', got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestValidateDefaultRetention(t *testing.T) { + tests := []struct { + name string + retention *DefaultRetention + expectError bool + errorMsg string + }{ + { + name: "Valid retention with days", + retention: &DefaultRetention{ + Mode: "GOVERNANCE", + Days: 30, + }, + expectError: false, + }, + { + name: "Valid retention with years", + retention: &DefaultRetention{ + Mode: "COMPLIANCE", + Years: 1, + }, + expectError: false, + }, + { + name: "Missing mode", + retention: &DefaultRetention{ + Days: 30, + }, + expectError: true, + errorMsg: "default retention must specify Mode", + }, + { + name: "Invalid mode", + retention: &DefaultRetention{ + Mode: "INVALID", + Days: 30, + }, + expectError: true, + errorMsg: "invalid default retention mode", + }, + { + name: "Both days and years specified", + retention: &DefaultRetention{ + Mode: "GOVERNANCE", + Days: 30, + Years: 1, + }, + expectError: true, + errorMsg: "default retention cannot specify both Days and Years", + }, + { + name: "Neither days nor years specified", + retention: &DefaultRetention{ + Mode: "GOVERNANCE", + }, + expectError: true, + errorMsg: "default retention must specify either Days or Years", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDefaultRetention(tt.retention) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error message to contain '%s', got: %v", tt.errorMsg, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + }) + } +} + +// Helper function to create a time pointer +func timePtr(t time.Time) *time.Time { + return &t +} |
