aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_object_retention_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/s3api_object_retention_test.go')
-rw-r--r--weed/s3api/s3api_object_retention_test.go726
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
+}