| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- package retention
- import (
- "context"
- "fmt"
- "strings"
- "testing"
- "time"
- "github.com/aws/aws-sdk-go-v2/aws"
- "github.com/aws/aws-sdk-go-v2/config"
- "github.com/aws/aws-sdk-go-v2/credentials"
- "github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/aws/aws-sdk-go-v2/service/s3/types"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // S3TestConfig holds configuration for S3 tests
- type S3TestConfig struct {
- Endpoint string
- AccessKey string
- SecretKey string
- Region string
- BucketPrefix string
- UseSSL bool
- SkipVerifySSL bool
- }
- // Default test configuration - should match test_config.json
- var defaultConfig = &S3TestConfig{
- Endpoint: "http://localhost:8333", // Default SeaweedFS S3 port
- AccessKey: "some_access_key1",
- SecretKey: "some_secret_key1",
- Region: "us-east-1",
- BucketPrefix: "test-retention-",
- UseSSL: false,
- SkipVerifySSL: true,
- }
- // getS3Client creates an AWS S3 client for testing
- func getS3Client(t *testing.T) *s3.Client {
- cfg, err := config.LoadDefaultConfig(context.TODO(),
- config.WithRegion(defaultConfig.Region),
- config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
- defaultConfig.AccessKey,
- defaultConfig.SecretKey,
- "",
- )),
- config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(
- func(service, region string, options ...interface{}) (aws.Endpoint, error) {
- return aws.Endpoint{
- URL: defaultConfig.Endpoint,
- SigningRegion: defaultConfig.Region,
- HostnameImmutable: true,
- }, nil
- })),
- )
- require.NoError(t, err)
- return s3.NewFromConfig(cfg, func(o *s3.Options) {
- o.UsePathStyle = true // Important for SeaweedFS
- })
- }
- // getNewBucketName generates a unique bucket name
- func getNewBucketName() string {
- timestamp := time.Now().UnixNano()
- return fmt.Sprintf("%s%d", defaultConfig.BucketPrefix, timestamp)
- }
- // createBucket creates a new bucket for testing
- func createBucket(t *testing.T, client *s3.Client, bucketName string) {
- _, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(t, err)
- }
- // deleteBucket deletes a bucket and all its contents
- func deleteBucket(t *testing.T, client *s3.Client, bucketName string) {
- // First, try to delete all objects and versions
- err := deleteAllObjectVersions(t, client, bucketName)
- if err != nil {
- t.Logf("Warning: failed to delete all object versions in first attempt: %v", err)
- // Try once more in case of transient errors
- time.Sleep(500 * time.Millisecond)
- err = deleteAllObjectVersions(t, client, bucketName)
- if err != nil {
- t.Logf("Warning: failed to delete all object versions in second attempt: %v", err)
- }
- }
- // Wait a bit for eventual consistency
- time.Sleep(100 * time.Millisecond)
- // Try to delete the bucket multiple times in case of eventual consistency issues
- for retries := 0; retries < 3; retries++ {
- _, err = client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{
- Bucket: aws.String(bucketName),
- })
- if err == nil {
- t.Logf("Successfully deleted bucket %s", bucketName)
- return
- }
- t.Logf("Warning: failed to delete bucket %s (attempt %d): %v", bucketName, retries+1, err)
- if retries < 2 {
- time.Sleep(200 * time.Millisecond)
- }
- }
- }
- // deleteAllObjectVersions deletes all object versions in a bucket
- func deleteAllObjectVersions(t *testing.T, client *s3.Client, bucketName string) error {
- // List all object versions
- paginator := s3.NewListObjectVersionsPaginator(client, &s3.ListObjectVersionsInput{
- Bucket: aws.String(bucketName),
- })
- for paginator.HasMorePages() {
- page, err := paginator.NextPage(context.TODO())
- if err != nil {
- return err
- }
- var objectsToDelete []types.ObjectIdentifier
- // Add versions - first try to remove retention/legal hold
- for _, version := range page.Versions {
- // Try to remove legal hold if present
- _, err := client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: version.Key,
- VersionId: version.VersionId,
- LegalHold: &types.ObjectLockLegalHold{
- Status: types.ObjectLockLegalHoldStatusOff,
- },
- })
- if err != nil {
- // Legal hold might not be set, ignore error
- t.Logf("Note: could not remove legal hold for %s@%s: %v", *version.Key, *version.VersionId, err)
- }
- objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
- Key: version.Key,
- VersionId: version.VersionId,
- })
- }
- // Add delete markers
- for _, deleteMarker := range page.DeleteMarkers {
- objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{
- Key: deleteMarker.Key,
- VersionId: deleteMarker.VersionId,
- })
- }
- // Delete objects in batches with bypass governance retention
- if len(objectsToDelete) > 0 {
- _, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
- Bucket: aws.String(bucketName),
- BypassGovernanceRetention: aws.Bool(true),
- Delete: &types.Delete{
- Objects: objectsToDelete,
- Quiet: aws.Bool(true),
- },
- })
- if err != nil {
- t.Logf("Warning: batch delete failed, trying individual deletion: %v", err)
- // Try individual deletion for each object
- for _, obj := range objectsToDelete {
- _, delErr := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: obj.Key,
- VersionId: obj.VersionId,
- BypassGovernanceRetention: aws.Bool(true),
- })
- if delErr != nil {
- t.Logf("Warning: failed to delete object %s@%s: %v", *obj.Key, *obj.VersionId, delErr)
- }
- }
- }
- }
- }
- return nil
- }
- // enableVersioning enables versioning on a bucket
- func enableVersioning(t *testing.T, client *s3.Client, bucketName string) {
- _, err := client.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
- Bucket: aws.String(bucketName),
- VersioningConfiguration: &types.VersioningConfiguration{
- Status: types.BucketVersioningStatusEnabled,
- },
- })
- require.NoError(t, err)
- }
- // putObject puts an object into a bucket
- func putObject(t *testing.T, client *s3.Client, bucketName, key, content string) *s3.PutObjectOutput {
- resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- Body: strings.NewReader(content),
- })
- require.NoError(t, err)
- return resp
- }
- // cleanupAllTestBuckets cleans up any leftover test buckets
- func cleanupAllTestBuckets(t *testing.T, client *s3.Client) {
- // List all buckets
- listResp, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{})
- if err != nil {
- t.Logf("Warning: failed to list buckets for cleanup: %v", err)
- return
- }
- // Delete buckets that match our test prefix
- for _, bucket := range listResp.Buckets {
- if bucket.Name != nil && strings.HasPrefix(*bucket.Name, defaultConfig.BucketPrefix) {
- t.Logf("Cleaning up leftover test bucket: %s", *bucket.Name)
- deleteBucket(t, client, *bucket.Name)
- }
- }
- }
- // TestBasicRetentionWorkflow tests the basic retention functionality
- func TestBasicRetentionWorkflow(t *testing.T) {
- client := getS3Client(t)
- bucketName := getNewBucketName()
- // Create bucket
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- // Enable versioning (required for retention)
- enableVersioning(t, client, bucketName)
- // Create object
- key := "test-object"
- content := "test content for retention"
- putResp := putObject(t, client, bucketName, key, content)
- require.NotNil(t, putResp.VersionId)
- // Set retention with GOVERNANCE mode
- retentionUntil := time.Now().Add(24 * time.Hour)
- _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- Retention: &types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeGovernance,
- RetainUntilDate: aws.Time(retentionUntil),
- },
- })
- require.NoError(t, err)
- // Get retention and verify it was set correctly
- retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.NoError(t, err)
- assert.Equal(t, types.ObjectLockRetentionModeGovernance, retentionResp.Retention.Mode)
- assert.WithinDuration(t, retentionUntil, *retentionResp.Retention.RetainUntilDate, time.Second)
- // Try to delete object without bypass - should fail
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.Error(t, err)
- // Delete object with bypass governance - should succeed
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- BypassGovernanceRetention: aws.Bool(true),
- })
- require.NoError(t, err)
- }
- // TestRetentionModeCompliance tests COMPLIANCE mode retention
- func TestRetentionModeCompliance(t *testing.T) {
- client := getS3Client(t)
- bucketName := getNewBucketName()
- // Create bucket and enable versioning
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- enableVersioning(t, client, bucketName)
- // Create object
- key := "compliance-test-object"
- content := "compliance test content"
- putResp := putObject(t, client, bucketName, key, content)
- require.NotNil(t, putResp.VersionId)
- // Set retention with COMPLIANCE mode
- retentionUntil := time.Now().Add(1 * time.Hour)
- _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- Retention: &types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeCompliance,
- RetainUntilDate: aws.Time(retentionUntil),
- },
- })
- require.NoError(t, err)
- // Get retention and verify
- retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.NoError(t, err)
- assert.Equal(t, types.ObjectLockRetentionModeCompliance, retentionResp.Retention.Mode)
- // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
- // Try DELETE with version ID - should fail for COMPLIANCE mode
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- })
- require.Error(t, err, "DELETE with version ID should be blocked by COMPLIANCE retention")
- // Try DELETE with version ID and bypass - should still fail (COMPLIANCE mode ignores bypass)
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- BypassGovernanceRetention: aws.Bool(true),
- })
- require.Error(t, err, "COMPLIANCE mode should ignore governance bypass")
- }
- // TestLegalHoldWorkflow tests legal hold functionality
- func TestLegalHoldWorkflow(t *testing.T) {
- client := getS3Client(t)
- bucketName := getNewBucketName()
- // Create bucket and enable versioning
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- enableVersioning(t, client, bucketName)
- // Create object
- key := "legal-hold-test-object"
- content := "legal hold test content"
- putResp := putObject(t, client, bucketName, key, content)
- require.NotNil(t, putResp.VersionId)
- // Set legal hold ON
- _, err := client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- LegalHold: &types.ObjectLockLegalHold{
- Status: types.ObjectLockLegalHoldStatusOn,
- },
- })
- require.NoError(t, err)
- // Get legal hold and verify
- legalHoldResp, err := client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.NoError(t, err)
- assert.Equal(t, types.ObjectLockLegalHoldStatusOn, legalHoldResp.LegalHold.Status)
- // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
- // Try DELETE with version ID - should fail due to legal hold
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- })
- require.Error(t, err, "DELETE with version ID should be blocked by legal hold")
- // Remove legal hold (must specify version ID since latest version is now delete marker)
- _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- LegalHold: &types.ObjectLockLegalHold{
- Status: types.ObjectLockLegalHoldStatusOff,
- },
- })
- require.NoError(t, err)
- // Verify legal hold is off (must specify version ID)
- legalHoldResp, err = client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- })
- require.NoError(t, err)
- assert.Equal(t, types.ObjectLockLegalHoldStatusOff, legalHoldResp.LegalHold.Status)
- // Now DELETE with version ID should succeed after legal hold removed
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- })
- require.NoError(t, err, "DELETE with version ID should succeed after legal hold removed")
- }
- // TestObjectLockConfiguration tests bucket object lock configuration
- func TestObjectLockConfiguration(t *testing.T) {
- client := getS3Client(t)
- // Use a more unique bucket name to avoid conflicts
- bucketName := fmt.Sprintf("object-lock-config-%d-%d", time.Now().UnixNano(), time.Now().UnixMilli()%10000)
- // Create bucket and enable versioning
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- enableVersioning(t, client, bucketName)
- // Set object lock configuration
- _, err := client.PutObjectLockConfiguration(context.TODO(), &s3.PutObjectLockConfigurationInput{
- Bucket: aws.String(bucketName),
- ObjectLockConfiguration: &types.ObjectLockConfiguration{
- ObjectLockEnabled: types.ObjectLockEnabledEnabled,
- Rule: &types.ObjectLockRule{
- DefaultRetention: &types.DefaultRetention{
- Mode: types.ObjectLockRetentionModeGovernance,
- Days: aws.Int32(30),
- },
- },
- },
- })
- if err != nil {
- t.Logf("PutObjectLockConfiguration failed (may not be supported): %v", err)
- t.Skip("Object lock configuration not supported, skipping test")
- return
- }
- // Get object lock configuration and verify
- configResp, err := client.GetObjectLockConfiguration(context.TODO(), &s3.GetObjectLockConfigurationInput{
- Bucket: aws.String(bucketName),
- })
- require.NoError(t, err)
- assert.Equal(t, types.ObjectLockEnabledEnabled, configResp.ObjectLockConfiguration.ObjectLockEnabled)
- require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention, "DefaultRetention should not be nil")
- require.NotNil(t, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days, "Days should not be nil")
- assert.Equal(t, types.ObjectLockRetentionModeGovernance, configResp.ObjectLockConfiguration.Rule.DefaultRetention.Mode)
- assert.Equal(t, int32(30), *configResp.ObjectLockConfiguration.Rule.DefaultRetention.Days)
- }
- // TestRetentionWithVersions tests retention with specific object versions
- func TestRetentionWithVersions(t *testing.T) {
- client := getS3Client(t)
- bucketName := getNewBucketName()
- // Create bucket and enable versioning
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- enableVersioning(t, client, bucketName)
- // Create multiple versions of the same object
- key := "versioned-retention-test"
- content1 := "version 1 content"
- content2 := "version 2 content"
- putResp1 := putObject(t, client, bucketName, key, content1)
- require.NotNil(t, putResp1.VersionId)
- putResp2 := putObject(t, client, bucketName, key, content2)
- require.NotNil(t, putResp2.VersionId)
- // Set retention on first version only
- retentionUntil := time.Now().Add(1 * time.Hour)
- _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp1.VersionId,
- Retention: &types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeGovernance,
- RetainUntilDate: aws.Time(retentionUntil),
- },
- })
- require.NoError(t, err)
- // Get retention for first version
- retentionResp, err := client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp1.VersionId,
- })
- require.NoError(t, err)
- assert.Equal(t, types.ObjectLockRetentionModeGovernance, retentionResp.Retention.Mode)
- // Try to get retention for second version - should fail (no retention set)
- _, err = client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp2.VersionId,
- })
- require.Error(t, err)
- // Delete second version should succeed (no retention)
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp2.VersionId,
- })
- require.NoError(t, err)
- // Delete first version should fail (has retention)
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp1.VersionId,
- })
- require.Error(t, err)
- // Delete first version with bypass should succeed
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp1.VersionId,
- BypassGovernanceRetention: aws.Bool(true),
- })
- require.NoError(t, err)
- }
- // TestRetentionAndLegalHoldCombination tests retention and legal hold together
- func TestRetentionAndLegalHoldCombination(t *testing.T) {
- client := getS3Client(t)
- bucketName := getNewBucketName()
- // Create bucket and enable versioning
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- enableVersioning(t, client, bucketName)
- // Create object
- key := "combined-protection-test"
- content := "combined protection test content"
- putResp := putObject(t, client, bucketName, key, content)
- require.NotNil(t, putResp.VersionId)
- // Set both retention and legal hold
- retentionUntil := time.Now().Add(1 * time.Hour)
- // Set retention
- _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- Retention: &types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeGovernance,
- RetainUntilDate: aws.Time(retentionUntil),
- },
- })
- require.NoError(t, err)
- // Set legal hold
- _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- LegalHold: &types.ObjectLockLegalHold{
- Status: types.ObjectLockLegalHoldStatusOn,
- },
- })
- require.NoError(t, err)
- // Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
- // Try DELETE with version ID and bypass - should still fail due to legal hold
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- BypassGovernanceRetention: aws.Bool(true),
- })
- require.Error(t, err, "Legal hold should prevent deletion even with governance bypass")
- // Remove legal hold (must specify version ID since latest version is now delete marker)
- _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- LegalHold: &types.ObjectLockLegalHold{
- Status: types.ObjectLockLegalHoldStatusOff,
- },
- })
- require.NoError(t, err)
- // Now DELETE with version ID and bypass governance should succeed
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- VersionId: putResp.VersionId,
- BypassGovernanceRetention: aws.Bool(true),
- })
- require.NoError(t, err, "DELETE with version ID should succeed after legal hold removed and with governance bypass")
- }
- // TestExpiredRetention tests that objects can be deleted after retention expires
- func TestExpiredRetention(t *testing.T) {
- client := getS3Client(t)
- bucketName := getNewBucketName()
- // Create bucket and enable versioning
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- enableVersioning(t, client, bucketName)
- // Create object
- key := "expired-retention-test"
- content := "expired retention test content"
- putResp := putObject(t, client, bucketName, key, content)
- require.NotNil(t, putResp.VersionId)
- // Set retention for a very short time (2 seconds)
- retentionUntil := time.Now().Add(2 * time.Second)
- _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- Retention: &types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeGovernance,
- RetainUntilDate: aws.Time(retentionUntil),
- },
- })
- require.NoError(t, err)
- // Try to delete immediately - should fail
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.Error(t, err)
- // Wait for retention to expire
- time.Sleep(3 * time.Second)
- // Now delete should succeed
- _, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- })
- require.NoError(t, err)
- }
- // TestRetentionErrorCases tests various error conditions
- func TestRetentionErrorCases(t *testing.T) {
- client := getS3Client(t)
- bucketName := getNewBucketName()
- // Create bucket and enable versioning
- createBucket(t, client, bucketName)
- defer deleteBucket(t, client, bucketName)
- enableVersioning(t, client, bucketName)
- // Test setting retention on non-existent object
- _, err := client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String("non-existent-key"),
- Retention: &types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeGovernance,
- RetainUntilDate: aws.Time(time.Now().Add(1 * time.Hour)),
- },
- })
- require.Error(t, err)
- // Test getting retention on non-existent object
- _, err = client.GetObjectRetention(context.TODO(), &s3.GetObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String("non-existent-key"),
- })
- require.Error(t, err)
- // Test setting legal hold on non-existent object
- _, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String("non-existent-key"),
- LegalHold: &types.ObjectLockLegalHold{
- Status: types.ObjectLockLegalHoldStatusOn,
- },
- })
- require.Error(t, err)
- // Test getting legal hold on non-existent object
- _, err = client.GetObjectLegalHold(context.TODO(), &s3.GetObjectLegalHoldInput{
- Bucket: aws.String(bucketName),
- Key: aws.String("non-existent-key"),
- })
- require.Error(t, err)
- // Test setting retention with past date
- key := "retention-past-date-test"
- content := "test content"
- putObject(t, client, bucketName, key, content)
- pastDate := time.Now().Add(-1 * time.Hour)
- _, err = client.PutObjectRetention(context.TODO(), &s3.PutObjectRetentionInput{
- Bucket: aws.String(bucketName),
- Key: aws.String(key),
- Retention: &types.ObjectLockRetention{
- Mode: types.ObjectLockRetentionModeGovernance,
- RetainUntilDate: aws.Time(pastDate),
- },
- })
- require.Error(t, err)
- }
|