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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
|
package s3api
import (
"bytes"
"context"
"testing"
"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"
)
// TestSuspendedVersioningNullOverwrite tests the scenario where:
// 1. Create object before versioning is enabled (pre-versioning object)
// 2. Enable versioning, then suspend it
// 3. Overwrite the object (should replace the null version, not create duplicate)
// 4. List versions should show only 1 version with versionId "null"
//
// This test corresponds to: test_versioning_obj_plain_null_version_overwrite_suspended
func TestSuspendedVersioningNullOverwrite(t *testing.T) {
ctx := context.Background()
client := getS3Client(t)
// Create bucket
bucketName := getNewBucketName()
createBucket(t, client, bucketName)
defer deleteBucket(t, client, bucketName)
objectKey := "testobjbar"
// Step 1: Put object before versioning is configured (pre-versioning object)
content1 := []byte("foooz")
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(content1),
})
if err != nil {
t.Fatalf("Failed to create pre-versioning object: %v", err)
}
t.Logf("Created pre-versioning object")
// Step 2: Enable versioning
_, err = client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
Bucket: aws.String(bucketName),
VersioningConfiguration: &types.VersioningConfiguration{
Status: types.BucketVersioningStatusEnabled,
},
})
if err != nil {
t.Fatalf("Failed to enable versioning: %v", err)
}
t.Logf("Enabled versioning")
// Step 3: Suspend versioning
_, err = client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
Bucket: aws.String(bucketName),
VersioningConfiguration: &types.VersioningConfiguration{
Status: types.BucketVersioningStatusSuspended,
},
})
if err != nil {
t.Fatalf("Failed to suspend versioning: %v", err)
}
t.Logf("Suspended versioning")
// Step 4: Overwrite the object during suspended versioning
content2 := []byte("zzz")
putResp, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(content2),
})
if err != nil {
t.Fatalf("Failed to overwrite object during suspended versioning: %v", err)
}
// Verify no VersionId is returned for suspended versioning
if putResp.VersionId != nil {
t.Errorf("Suspended versioning should NOT return VersionId, but got: %s", *putResp.VersionId)
}
t.Logf("Overwrote object during suspended versioning (no VersionId returned as expected)")
// Step 5: Verify content is updated
getResp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
if err != nil {
t.Fatalf("Failed to get object: %v", err)
}
defer getResp.Body.Close()
gotContent := new(bytes.Buffer)
gotContent.ReadFrom(getResp.Body)
if !bytes.Equal(gotContent.Bytes(), content2) {
t.Errorf("Expected content %q, got %q", content2, gotContent.Bytes())
}
t.Logf("Object content is correctly updated to: %q", content2)
// Step 6: List object versions - should have only 1 version
listResp, err := client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: aws.String(bucketName),
})
if err != nil {
t.Fatalf("Failed to list object versions: %v", err)
}
// Count versions (excluding delete markers)
versionCount := len(listResp.Versions)
deleteMarkerCount := len(listResp.DeleteMarkers)
t.Logf("List results: %d versions, %d delete markers", versionCount, deleteMarkerCount)
for i, v := range listResp.Versions {
t.Logf(" Version %d: Key=%s, VersionId=%s, IsLatest=%v, Size=%d",
i, *v.Key, *v.VersionId, v.IsLatest, v.Size)
}
// THIS IS THE KEY ASSERTION: Should have exactly 1 version, not 2
if versionCount != 1 {
t.Errorf("Expected 1 version after suspended versioning overwrite, got %d versions", versionCount)
t.Error("BUG: Duplicate null versions detected! The overwrite should have replaced the pre-versioning object.")
} else {
t.Logf("PASS: Only 1 version found (no duplicate null versions)")
}
if deleteMarkerCount != 0 {
t.Errorf("Expected 0 delete markers, got %d", deleteMarkerCount)
}
// Verify the version has versionId "null"
if versionCount > 0 {
if listResp.Versions[0].VersionId == nil || *listResp.Versions[0].VersionId != "null" {
t.Errorf("Expected VersionId to be 'null', got %v", listResp.Versions[0].VersionId)
} else {
t.Logf("Version ID is 'null' as expected")
}
}
// Step 7: Delete the null version
_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
VersionId: aws.String("null"),
})
if err != nil {
t.Fatalf("Failed to delete null version: %v", err)
}
t.Logf("Deleted null version")
// Step 8: Verify object no longer exists
_, err = client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
})
if err == nil {
t.Error("Expected object to not exist after deleting null version")
}
t.Logf("Object no longer exists after deleting null version")
// Step 9: Verify no versions remain
listResp, err = client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: aws.String(bucketName),
})
if err != nil {
t.Fatalf("Failed to list object versions: %v", err)
}
if len(listResp.Versions) != 0 || len(listResp.DeleteMarkers) != 0 {
t.Errorf("Expected no versions or delete markers, got %d versions and %d delete markers",
len(listResp.Versions), len(listResp.DeleteMarkers))
} else {
t.Logf("No versions remain after deletion")
}
}
// TestEnabledVersioningReturnsVersionId tests that when versioning is ENABLED,
// every PutObject operation returns a version ID
//
// This test corresponds to the create_multiple_versions helper function
func TestEnabledVersioningReturnsVersionId(t *testing.T) {
ctx := context.Background()
client := getS3Client(t)
// Create bucket
bucketName := getNewBucketName()
createBucket(t, client, bucketName)
defer deleteBucket(t, client, bucketName)
objectKey := "testobj"
// Enable versioning
_, err := client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
Bucket: aws.String(bucketName),
VersioningConfiguration: &types.VersioningConfiguration{
Status: types.BucketVersioningStatusEnabled,
},
})
if err != nil {
t.Fatalf("Failed to enable versioning: %v", err)
}
t.Logf("Enabled versioning")
// Create multiple versions
numVersions := 3
versionIds := make([]string, 0, numVersions)
for i := 0; i < numVersions; i++ {
content := []byte("content-" + string(rune('0'+i)))
putResp, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: bytes.NewReader(content),
})
if err != nil {
t.Fatalf("Failed to create version %d: %v", i, err)
}
// THIS IS THE KEY ASSERTION: VersionId MUST be returned for enabled versioning
if putResp.VersionId == nil {
t.Errorf("FAILED: PutObject with enabled versioning MUST return VersionId, but got nil for version %d", i)
} else {
versionId := *putResp.VersionId
if versionId == "" {
t.Errorf("FAILED: PutObject returned empty VersionId for version %d", i)
} else if versionId == "null" {
t.Errorf("FAILED: PutObject with enabled versioning should NOT return 'null' version ID, got: %s", versionId)
} else {
versionIds = append(versionIds, versionId)
t.Logf("Version %d created with VersionId: %s", i, versionId)
}
}
}
if len(versionIds) != numVersions {
t.Errorf("Expected %d version IDs, got %d", numVersions, len(versionIds))
}
// List versions to verify all were created
listResp, err := client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: aws.String(bucketName),
})
if err != nil {
t.Fatalf("Failed to list object versions: %v", err)
}
if len(listResp.Versions) != numVersions {
t.Errorf("Expected %d versions in list, got %d", numVersions, len(listResp.Versions))
} else {
t.Logf("All %d versions are listed", numVersions)
}
// Verify all version IDs match
for i, v := range listResp.Versions {
t.Logf(" Version %d: VersionId=%s, Size=%d, IsLatest=%v", i, *v.VersionId, v.Size, v.IsLatest)
}
}
|