mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-12 09:30:46 +02:00
The suspended PUT dropped the null delete marker before writing, so a failed write left the .versions pointer naming a marker that was gone. The read path heals a dangling pointer by promoting the newest survivor, so a key the caller had deleted came back serving an older version, and the heal persisted that pointer. Move the retire into afterCreate via the shared finalize, which also brings the ownership check the copy and multipart paths already have.
298 lines
9.7 KiB
Go
298 lines
9.7 KiB
Go
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"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// The suspended PUT retires the null delete marker a preceding DELETE left, the same
|
|
// as the copy and multipart paths. While the regular-path object owns the null slot a
|
|
// leftover marker is shadowed, so it only shows once that null version goes away.
|
|
func TestSuspendedPutRetiresDeleteMarker(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucket(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
objectKey := "suspended-put-after-delete.txt"
|
|
|
|
enableVersioning(t, client, bucketName)
|
|
putObject(t, client, bucketName, objectKey, "pre-suspension-content")
|
|
suspendVersioning(t, client, bucketName)
|
|
|
|
putObject(t, client, bucketName, objectKey, "null-version-content")
|
|
deleteKey(t, client, bucketName, objectKey)
|
|
putObject(t, client, bucketName, objectKey, "replacement-content")
|
|
|
|
headObject(t, client, bucketName, objectKey)
|
|
|
|
// Drop the null version just written; a retired marker leaves nothing behind.
|
|
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
VersionId: aws.String("null"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
listResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String(objectKey),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, listResp.DeleteMarkers, "the suspended PUT should have retired the null delete marker")
|
|
}
|