Files
seaweedFS/weed/s3api/s3api_object_retention_test.go
Chris Lu 5f77f87335 S3: S3 Object Retention API to include XML namespace support (#7517)
* Refactor S3 Object Retention API to include XML namespace support and improve compatibility with Veeam. Updated XML tags to remove hardcoded namespaces and added test cases for retention and legal hold configurations without namespaces.

* Added XMLNS field setting in both places
2025-11-20 11:42:22 -08:00

814 lines
20 KiB
Go

package s3api
import (
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)
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: "Valid retention XML without namespace (Veeam compatibility)",
xmlBody: `<Retention>
<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 without namespace (Veeam compatibility)",
xmlBody: `<Retention>
<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: "Valid legal hold ON without namespace",
xmlBody: `<LegalHold>
<Status>ON</Status>
</LegalHold>`,
expectError: false,
expectedResult: &ObjectLegalHold{
Status: "ON",
},
},
{
name: "Valid legal hold OFF without namespace",
xmlBody: `<LegalHold>
<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: "Valid object lock configuration without namespace",
xmlBody: `<ObjectLockConfiguration>
<ObjectLockEnabled>Enabled</ObjectLockEnabled>
</ObjectLockConfiguration>`,
expectError: false,
expectedResult: &ObjectLockConfiguration{
ObjectLockEnabled: "Enabled",
},
},
{
name: "Valid object lock configuration with rule without namespace",
xmlBody: `<ObjectLockConfiguration>
<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,
DaysSet: true,
},
},
},
expectError: false,
},
{
name: "Valid config with rule and years",
config: &ObjectLockConfiguration{
ObjectLockEnabled: "Enabled",
Rule: &ObjectLockRule{
DefaultRetention: &DefaultRetention{
Mode: "COMPLIANCE",
Years: 1,
YearsSet: true,
},
},
},
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,
DaysSet: true,
YearsSet: true,
},
},
},
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,
DaysSet: true,
},
},
},
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,
DaysSet: true,
},
},
},
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,
YearsSet: true,
},
},
},
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,
DaysSet: true,
},
expectError: false,
},
{
name: "Valid retention with years",
retention: &DefaultRetention{
Mode: "COMPLIANCE",
Years: 1,
YearsSet: true,
},
expectError: false,
},
{
name: "Missing mode",
retention: &DefaultRetention{
Days: 30,
DaysSet: true,
},
expectError: true,
errorMsg: "default retention must specify Mode",
},
{
name: "Invalid mode",
retention: &DefaultRetention{
Mode: "INVALID",
Days: 30,
DaysSet: true,
},
expectError: true,
errorMsg: "invalid default retention mode",
},
{
name: "Both days and years specified",
retention: &DefaultRetention{
Mode: "GOVERNANCE",
Days: 30,
Years: 1,
DaysSet: true,
YearsSet: true,
},
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
}