aboutsummaryrefslogtreecommitdiff
path: root/test/s3/versioning/s3_versioning_object_lock_test.go
blob: 5c2689935e851de41ec7b2221c568d6cc31d95be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package s3api

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"
)

// TestVersioningWithObjectLockHeaders ensures that versioned objects properly
// handle object lock headers in PUT requests and return them in HEAD/GET responses.
// This test would have caught the bug where object lock metadata was not returned
// in HEAD/GET responses.
func TestVersioningWithObjectLockHeaders(t *testing.T) {
	client := getS3Client(t)
	bucketName := getNewBucketName()

	// Create bucket with object lock and versioning enabled
	createBucketWithObjectLock(t, client, bucketName)
	defer deleteBucket(t, client, bucketName)

	key := "versioned-object-with-lock"
	content1 := "version 1 content"
	content2 := "version 2 content"

	// PUT first version with object lock headers
	retainUntilDate1 := time.Now().Add(12 * time.Hour)
	putResp1, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
		Bucket:                    aws.String(bucketName),
		Key:                       aws.String(key),
		Body:                      strings.NewReader(content1),
		ObjectLockMode:            types.ObjectLockModeGovernance,
		ObjectLockRetainUntilDate: aws.Time(retainUntilDate1),
	})
	require.NoError(t, err)
	require.NotNil(t, putResp1.VersionId)

	// PUT second version with different object lock settings
	retainUntilDate2 := time.Now().Add(24 * time.Hour)
	putResp2, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
		Bucket:                    aws.String(bucketName),
		Key:                       aws.String(key),
		Body:                      strings.NewReader(content2),
		ObjectLockMode:            types.ObjectLockModeCompliance,
		ObjectLockRetainUntilDate: aws.Time(retainUntilDate2),
		ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatusOn,
	})
	require.NoError(t, err)
	require.NotNil(t, putResp2.VersionId)
	require.NotEqual(t, *putResp1.VersionId, *putResp2.VersionId)

	// Test HEAD latest version returns correct object lock metadata
	t.Run("HEAD latest version", func(t *testing.T) {
		headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
			Bucket: aws.String(bucketName),
			Key:    aws.String(key),
		})
		require.NoError(t, err)

		// Should return metadata for version 2 (latest)
		assert.Equal(t, types.ObjectLockModeCompliance, headResp.ObjectLockMode)
		assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
		assert.WithinDuration(t, retainUntilDate2, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
		assert.Equal(t, types.ObjectLockLegalHoldStatusOn, headResp.ObjectLockLegalHoldStatus)
	})

	// Test HEAD specific version returns correct object lock metadata
	t.Run("HEAD specific version", func(t *testing.T) {
		headResp, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
			Bucket:    aws.String(bucketName),
			Key:       aws.String(key),
			VersionId: putResp1.VersionId,
		})
		require.NoError(t, err)

		// Should return metadata for version 1
		assert.Equal(t, types.ObjectLockModeGovernance, headResp.ObjectLockMode)
		assert.NotNil(t, headResp.ObjectLockRetainUntilDate)
		assert.WithinDuration(t, retainUntilDate1, *headResp.ObjectLockRetainUntilDate, 5*time.Second)
		// Version 1 was created without legal hold, so AWS S3 defaults it to "OFF"
		assert.Equal(t, types.ObjectLockLegalHoldStatusOff, headResp.ObjectLockLegalHoldStatus)
	})

	// Test GET latest version returns correct object lock metadata
	t.Run("GET latest version", func(t *testing.T) {
		getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
			Bucket: aws.String(bucketName),
			Key:    aws.String(key),
		})
		require.NoError(t, err)
		defer getResp.Body.Close()

		// Should return metadata for version 2 (latest)
		assert.Equal(t, types.ObjectLockModeCompliance, getResp.ObjectLockMode)
		assert.NotNil(t, getResp.ObjectLockRetainUntilDate)
		assert.WithinDuration(t, retainUntilDate2, *getResp.ObjectLockRetainUntilDate, 5*time.Second)
		assert.Equal(t, types.ObjectLockLegalHoldStatusOn, getResp.ObjectLockLegalHoldStatus)
	})

	// Test GET specific version returns correct object lock metadata
	t.Run("GET specific version", func(t *testing.T) {
		getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
			Bucket:    aws.String(bucketName),
			Key:       aws.String(key),
			VersionId: putResp1.VersionId,
		})
		require.NoError(t, err)
		defer getResp.Body.Close()

		// Should return metadata for version 1
		assert.Equal(t, types.ObjectLockModeGovernance, getResp.ObjectLockMode)
		assert.NotNil(t, getResp.ObjectLockRetainUntilDate)
		assert.WithinDuration(t, retainUntilDate1, *getResp.ObjectLockRetainUntilDate, 5*time.Second)
		// Version 1 was created without legal hold, so AWS S3 defaults it to "OFF"
		assert.Equal(t, types.ObjectLockLegalHoldStatusOff, getResp.ObjectLockLegalHoldStatus)
	})
}

// waitForVersioningToBeEnabled polls the bucket versioning status until it's enabled
// This helps avoid race conditions where object lock is configured but versioning
// isn't immediately available
func waitForVersioningToBeEnabled(t *testing.T, client *s3.Client, bucketName string) {
	timeout := time.Now().Add(10 * time.Second)
	for time.Now().Before(timeout) {
		resp, err := client.GetBucketVersioning(context.TODO(), &s3.GetBucketVersioningInput{
			Bucket: aws.String(bucketName),
		})
		if err == nil && resp.Status == types.BucketVersioningStatusEnabled {
			return // Versioning is enabled
		}

		time.Sleep(100 * time.Millisecond)
	}
	t.Fatalf("Timeout waiting for versioning to be enabled on bucket %s", bucketName)
}

// Helper function for creating buckets 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)

	// Wait for versioning to be automatically enabled by object lock
	waitForVersioningToBeEnabled(t, client, bucketName)

	// Verify that object lock was actually enabled
	t.Logf("Verifying object lock configuration for bucket %s", bucketName)
	_, err = client.GetObjectLockConfiguration(context.TODO(), &s3.GetObjectLockConfigurationInput{
		Bucket: aws.String(bucketName),
	})
	require.NoError(t, err, "Object lock should be configured for bucket %s", bucketName)
}