s3api: make conditional mutations atomic and AWS-compatible (#8802)
* s3api: serialize conditional write finalization * s3api: add conditional delete mutation checks * s3api: enforce destination conditions for copy * s3api: revalidate multipart completion under lock * s3api: rollback failed put finalization hooks * s3api: report delete-marker version deletions * s3api: fix copy destination versioning edge cases * s3api: make versioned multipart completion idempotent * test/s3: cover conditional mutation regressions * s3api: rollback failed copy version finalization * s3api: resolve suspended delete conditions via latest entry * s3api: remove copy test null-version injection * s3api: reject out-of-order multipart completions * s3api: preserve multipart replay version metadata * s3api: surface copy destination existence errors * s3api: simplify delete condition target resolution * test/s3: make conditional delete assertions order independent * test/s3: add distributed lock gateway integration * s3api: fail closed multipart versioned completion * s3api: harden copy metadata and overwrite paths * s3api: create delete markers for suspended deletes * s3api: allow duplicate multipart completion parts
This commit is contained in:
135
test/s3/delete/s3_conditional_delete_test.go
Normal file
135
test/s3/delete/s3_conditional_delete_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"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/aws/smithy-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConditionalDeleteIfMatchOnLatestVersion(t *testing.T) {
|
||||
client := getTestClient(t)
|
||||
bucket := createTestBucket(t, client)
|
||||
defer cleanupBucket(t, client, bucket)
|
||||
|
||||
key := "conditional-delete.txt"
|
||||
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: bytes.NewReader([]byte("versioned body")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, putResp.ETag)
|
||||
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
IfMatch: aws.String(`"not-the-current-etag"`),
|
||||
})
|
||||
require.Error(t, err, "DeleteObject should reject a mismatched If-Match header")
|
||||
|
||||
var apiErr smithy.APIError
|
||||
if assert.True(t, errors.As(err, &apiErr), "Expected smithy API error for conditional delete") {
|
||||
assert.Equal(t, "PreconditionFailed", apiErr.ErrorCode())
|
||||
}
|
||||
|
||||
_, err = client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Object should remain current after a failed conditional delete")
|
||||
|
||||
deleteResp, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
IfMatch: putResp.ETag,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, deleteResp.DeleteMarker)
|
||||
assert.True(t, *deleteResp.DeleteMarker, "Successful conditional delete on a versioned bucket should create a delete marker")
|
||||
require.NotNil(t, deleteResp.VersionId)
|
||||
|
||||
_, err = client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.Error(t, err, "Delete marker should hide the current object after a successful conditional delete")
|
||||
}
|
||||
|
||||
func TestConditionalMultiDeletePerObjectETag(t *testing.T) {
|
||||
client := getTestClient(t)
|
||||
bucket := createTestBucket(t, client)
|
||||
defer cleanupBucket(t, client, bucket)
|
||||
|
||||
okKey := "delete-ok.txt"
|
||||
failKey := "delete-fail.txt"
|
||||
|
||||
okPutResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(okKey),
|
||||
Body: bytes.NewReader([]byte("delete me")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, okPutResp.ETag)
|
||||
|
||||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(failKey),
|
||||
Body: bytes.NewReader([]byte("keep me")),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
deleteResp, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Delete: &types.Delete{
|
||||
Objects: []types.ObjectIdentifier{
|
||||
{
|
||||
Key: aws.String(okKey),
|
||||
ETag: okPutResp.ETag,
|
||||
},
|
||||
{
|
||||
Key: aws.String(failKey),
|
||||
ETag: aws.String(`"mismatched-etag"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, deleteResp.Deleted, 1, "One object should satisfy its ETag precondition")
|
||||
require.Len(t, deleteResp.Errors, 1, "One object should report a precondition failure")
|
||||
deletedKeys := make([]string, 0, len(deleteResp.Deleted))
|
||||
for _, deleted := range deleteResp.Deleted {
|
||||
deletedKeys = append(deletedKeys, aws.ToString(deleted.Key))
|
||||
}
|
||||
assert.Contains(t, deletedKeys, okKey)
|
||||
|
||||
var matchedError *types.Error
|
||||
for i := range deleteResp.Errors {
|
||||
if aws.ToString(deleteResp.Errors[i].Key) == failKey {
|
||||
matchedError = &deleteResp.Errors[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if assert.NotNil(t, matchedError, "Expected error entry for failed key") {
|
||||
assert.Equal(t, "PreconditionFailed", aws.ToString(matchedError.Code))
|
||||
}
|
||||
|
||||
_, err = client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(okKey),
|
||||
})
|
||||
require.Error(t, err, "Successfully deleted key should no longer be current")
|
||||
|
||||
_, err = client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(failKey),
|
||||
})
|
||||
require.NoError(t, err, "Object with mismatched ETag should remain untouched")
|
||||
}
|
||||
Reference in New Issue
Block a user