You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							307 lines
						
					
					
						
							10 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							307 lines
						
					
					
						
							10 KiB
						
					
					
				| package retention | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"strings" | |
| 	"testing" | |
| 	"time" | |
| 
 | |
| 	"github.com/aws/aws-sdk-go-v2/aws" | |
| 	"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" | |
| ) | |
| 
 | |
| // TestPutObjectWithLockHeaders tests that object lock headers in PUT requests | |
| // are properly stored and returned in HEAD responses | |
| func TestPutObjectWithLockHeaders(t *testing.T) { | |
| 	client := getS3Client(t) | |
| 	bucketName := getNewBucketName() | |
| 
 | |
| 	// Create bucket with object lock enabled and versioning | |
| 	createBucketWithObjectLock(t, client, bucketName) | |
| 	defer deleteBucket(t, client, bucketName) | |
| 
 | |
| 	key := "test-object-lock-headers" | |
| 	content := "test content with object lock headers" | |
| 	retainUntilDate := time.Now().Add(24 * time.Hour) | |
| 
 | |
| 	// Test 1: PUT with COMPLIANCE mode and retention date | |
| 	t.Run("PUT with COMPLIANCE mode", func(t *testing.T) { | |
| 		testKey := key + "-compliance" | |
| 
 | |
| 		// PUT object with lock headers | |
| 		putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content, | |
| 			"COMPLIANCE", retainUntilDate, "") | |
| 		require.NotNil(t, putResp.VersionId) | |
| 
 | |
| 		// HEAD object and verify lock headers are returned | |
| 		headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{ | |
| 			Bucket: aws.String(bucketName), | |
| 			Key:    aws.String(testKey), | |
| 		}) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		// Verify object lock metadata is present in response | |
| 		assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode) | |
| 		assert.NotNil(t, headResp.ObjectLockRetainUntilDate) | |
| 		assert.WithinDuration(t, retainUntilDate, *headResp.ObjectLockRetainUntilDate, 5*time.Second) | |
| 	}) | |
| 
 | |
| 	// Test 2: PUT with GOVERNANCE mode and retention date | |
| 	t.Run("PUT with GOVERNANCE mode", func(t *testing.T) { | |
| 		testKey := key + "-governance" | |
| 
 | |
| 		putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content, | |
| 			"GOVERNANCE", retainUntilDate, "") | |
| 		require.NotNil(t, putResp.VersionId) | |
| 
 | |
| 		headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{ | |
| 			Bucket: aws.String(bucketName), | |
| 			Key:    aws.String(testKey), | |
| 		}) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode) | |
| 		assert.NotNil(t, headResp.ObjectLockRetainUntilDate) | |
| 		assert.WithinDuration(t, retainUntilDate, *headResp.ObjectLockRetainUntilDate, 5*time.Second) | |
| 	}) | |
| 
 | |
| 	// Test 3: PUT with legal hold | |
| 	t.Run("PUT with legal hold", func(t *testing.T) { | |
| 		testKey := key + "-legal-hold" | |
| 
 | |
| 		putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content, | |
| 			"", time.Time{}, "ON") | |
| 		require.NotNil(t, putResp.VersionId) | |
| 
 | |
| 		headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{ | |
| 			Bucket: aws.String(bucketName), | |
| 			Key:    aws.String(testKey), | |
| 		}) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus) | |
| 	}) | |
| 
 | |
| 	// Test 4: PUT with both retention and legal hold | |
| 	t.Run("PUT with both retention and legal hold", func(t *testing.T) { | |
| 		testKey := key + "-both" | |
| 
 | |
| 		putResp := putObjectWithLockHeaders(t, client, bucketName, testKey, content, | |
| 			"GOVERNANCE", retainUntilDate, "ON") | |
| 		require.NotNil(t, putResp.VersionId) | |
| 
 | |
| 		headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{ | |
| 			Bucket: aws.String(bucketName), | |
| 			Key:    aws.String(testKey), | |
| 		}) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode) | |
| 		assert.NotNil(t, headResp.ObjectLockRetainUntilDate) | |
| 		assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus) | |
| 	}) | |
| } | |
| 
 | |
| // TestGetObjectWithLockHeaders verifies that GET requests also return object lock metadata | |
| func TestGetObjectWithLockHeaders(t *testing.T) { | |
| 	client := getS3Client(t) | |
| 	bucketName := getNewBucketName() | |
| 
 | |
| 	createBucketWithObjectLock(t, client, bucketName) | |
| 	defer deleteBucket(t, client, bucketName) | |
| 
 | |
| 	key := "test-get-object-lock" | |
| 	content := "test content for GET with lock headers" | |
| 	retainUntilDate := time.Now().Add(24 * time.Hour) | |
| 
 | |
| 	// PUT object with lock headers | |
| 	putResp := putObjectWithLockHeaders(t, client, bucketName, key, content, | |
| 		"COMPLIANCE", retainUntilDate, "ON") | |
| 	require.NotNil(t, putResp.VersionId) | |
| 
 | |
| 	// GET object and verify lock headers are returned | |
| 	getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		Key:    aws.String(key), | |
| 	}) | |
| 	require.NoError(t, err) | |
| 	defer getResp.Body.Close() | |
| 
 | |
| 	// Verify object lock metadata is present in GET response | |
| 	assert.Equal(t, types.ObjectLockModeCompliance, getResp.ObjectLockMode) | |
| 	assert.NotNil(t, getResp.ObjectLockRetainUntilDate) | |
| 	assert.WithinDuration(t, retainUntilDate, *getResp.ObjectLockRetainUntilDate, 5*time.Second) | |
| 	assert.Equal(t, types.ObjectLockLegalHoldStatusOn, getResp.ObjectLockLegalHoldStatus) | |
| } | |
| 
 | |
| // TestVersionedObjectLockHeaders tests object lock headers work with versioned objects | |
| func TestVersionedObjectLockHeaders(t *testing.T) { | |
| 	client := getS3Client(t) | |
| 	bucketName := getNewBucketName() | |
| 
 | |
| 	createBucketWithObjectLock(t, client, bucketName) | |
| 	defer deleteBucket(t, client, bucketName) | |
| 
 | |
| 	key := "test-versioned-lock" | |
| 	content1 := "version 1 content" | |
| 	content2 := "version 2 content" | |
| 	retainUntilDate1 := time.Now().Add(12 * time.Hour) | |
| 	retainUntilDate2 := time.Now().Add(24 * time.Hour) | |
| 
 | |
| 	// PUT first version with GOVERNANCE mode | |
| 	putResp1 := putObjectWithLockHeaders(t, client, bucketName, key, content1, | |
| 		"GOVERNANCE", retainUntilDate1, "") | |
| 	require.NotNil(t, putResp1.VersionId) | |
| 
 | |
| 	// PUT second version with COMPLIANCE mode | |
| 	putResp2 := putObjectWithLockHeaders(t, client, bucketName, key, content2, | |
| 		"COMPLIANCE", retainUntilDate2, "ON") | |
| 	require.NotNil(t, putResp2.VersionId) | |
| 	require.NotEqual(t, *putResp1.VersionId, *putResp2.VersionId) | |
| 
 | |
| 	// HEAD latest version (version 2) | |
| 	headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		Key:    aws.String(key), | |
| 	}) | |
| 	require.NoError(t, err) | |
| 	assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode) | |
| 	assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus) | |
| 
 | |
| 	// HEAD specific version 1 | |
| 	headResp1, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{ | |
| 		Bucket:    aws.String(bucketName), | |
| 		Key:       aws.String(key), | |
| 		VersionId: putResp1.VersionId, | |
| 	}) | |
| 	require.NoError(t, err) | |
| 	assert.Equal(t, types.ObjectLockModeGovernance, headResp1.ObjectLockMode) | |
| 	assert.NotEqual(t, types.ObjectLockLegalHoldStatusOn, headResp1.ObjectLockLegalHoldStatus) | |
| } | |
| 
 | |
| // TestObjectLockHeadersErrorCases tests various error scenarios | |
| func TestObjectLockHeadersErrorCases(t *testing.T) { | |
| 	client := getS3Client(t) | |
| 	bucketName := getNewBucketName() | |
| 
 | |
| 	createBucketWithObjectLock(t, client, bucketName) | |
| 	defer deleteBucket(t, client, bucketName) | |
| 
 | |
| 	key := "test-error-cases" | |
| 	content := "test content for error cases" | |
| 
 | |
| 	// Test 1: Invalid retention mode should be rejected | |
| 	t.Run("Invalid retention mode", func(t *testing.T) { | |
| 		_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ | |
| 			Bucket:                    aws.String(bucketName), | |
| 			Key:                       aws.String(key + "-invalid-mode"), | |
| 			Body:                      strings.NewReader(content), | |
| 			ObjectLockMode:            "INVALID_MODE", // Invalid mode | |
| 			ObjectLockRetainUntilDate: aws.Time(time.Now().Add(24 * time.Hour)), | |
| 		}) | |
| 		require.Error(t, err) | |
| 	}) | |
| 
 | |
| 	// Test 2: Retention date in the past should be rejected | |
| 	t.Run("Past retention date", func(t *testing.T) { | |
| 		_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ | |
| 			Bucket:                    aws.String(bucketName), | |
| 			Key:                       aws.String(key + "-past-date"), | |
| 			Body:                      strings.NewReader(content), | |
| 			ObjectLockMode:            "GOVERNANCE", | |
| 			ObjectLockRetainUntilDate: aws.Time(time.Now().Add(-24 * time.Hour)), // Past date | |
| 		}) | |
| 		require.Error(t, err) | |
| 	}) | |
| 
 | |
| 	// Test 3: Mode without date should be rejected | |
| 	t.Run("Mode without retention date", func(t *testing.T) { | |
| 		_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ | |
| 			Bucket:         aws.String(bucketName), | |
| 			Key:            aws.String(key + "-no-date"), | |
| 			Body:           strings.NewReader(content), | |
| 			ObjectLockMode: "GOVERNANCE", | |
| 			// Missing ObjectLockRetainUntilDate | |
| 		}) | |
| 		require.Error(t, err) | |
| 	}) | |
| } | |
| 
 | |
| // TestObjectLockHeadersNonVersionedBucket tests that object lock fails on non-versioned buckets | |
| func TestObjectLockHeadersNonVersionedBucket(t *testing.T) { | |
| 	client := getS3Client(t) | |
| 	bucketName := getNewBucketName() | |
| 
 | |
| 	// Create regular bucket without object lock/versioning | |
| 	createBucket(t, client, bucketName) | |
| 	defer deleteBucket(t, client, bucketName) | |
| 
 | |
| 	key := "test-non-versioned" | |
| 	content := "test content" | |
| 	retainUntilDate := time.Now().Add(24 * time.Hour) | |
| 
 | |
| 	// Attempting to PUT with object lock headers should fail | |
| 	_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ | |
| 		Bucket:                    aws.String(bucketName), | |
| 		Key:                       aws.String(key), | |
| 		Body:                      strings.NewReader(content), | |
| 		ObjectLockMode:            "GOVERNANCE", | |
| 		ObjectLockRetainUntilDate: aws.Time(retainUntilDate), | |
| 	}) | |
| 	require.Error(t, err) | |
| } | |
| 
 | |
| // Helper Functions | |
|  | |
| // putObjectWithLockHeaders puts an object with object lock headers | |
| func putObjectWithLockHeaders(t *testing.T, client *s3.Client, bucketName, key, content string, | |
| 	mode string, retainUntilDate time.Time, legalHold string) *s3.PutObjectOutput { | |
| 
 | |
| 	input := &s3.PutObjectInput{ | |
| 		Bucket: aws.String(bucketName), | |
| 		Key:    aws.String(key), | |
| 		Body:   strings.NewReader(content), | |
| 	} | |
| 
 | |
| 	// Add retention mode and date if specified | |
| 	if mode != "" { | |
| 		switch mode { | |
| 		case "COMPLIANCE": | |
| 			input.ObjectLockMode = types.ObjectLockModeCompliance | |
| 		case "GOVERNANCE": | |
| 			input.ObjectLockMode = types.ObjectLockModeGovernance | |
| 		} | |
| 		if !retainUntilDate.IsZero() { | |
| 			input.ObjectLockRetainUntilDate = aws.Time(retainUntilDate) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Add legal hold if specified | |
| 	if legalHold != "" { | |
| 		switch legalHold { | |
| 		case "ON": | |
| 			input.ObjectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOn | |
| 		case "OFF": | |
| 			input.ObjectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOff | |
| 		} | |
| 	} | |
| 
 | |
| 	resp, err := client.PutObject(context.TODO(), input) | |
| 	require.NoError(t, err) | |
| 	return resp | |
| } | |
| 
 | |
| // createBucketWithObjectLock creates a bucket with object lock enabled | |
| func createBucketWithObjectLock(t *testing.T, client *s3.Client, bucketName string) { | |
| 	_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{ | |
| 		Bucket:                     aws.String(bucketName), | |
| 		ObjectLockEnabledForBucket: aws.Bool(true), | |
| 	}) | |
| 	require.NoError(t, err) | |
| 
 | |
| 	// Enable versioning (required for object lock) | |
| 	enableVersioning(t, client, bucketName) | |
| }
 |