* 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
120 lines
3.2 KiB
Go
120 lines
3.2 KiB
Go
package s3api
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
func TestValidateDeleteIfMatch(t *testing.T) {
|
|
s3a := NewS3ApiServerForTest()
|
|
existingEntry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.ExtETagKey: []byte("\"abc123\""),
|
|
},
|
|
}
|
|
deleteMarkerEntry := &filer_pb.Entry{
|
|
Extended: map[string][]byte{
|
|
s3_constants.ExtDeleteMarkerKey: []byte("true"),
|
|
},
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
entry *filer_pb.Entry
|
|
ifMatch string
|
|
missingCode s3err.ErrorCode
|
|
expected s3err.ErrorCode
|
|
}{
|
|
{
|
|
name: "matching etag succeeds",
|
|
entry: existingEntry,
|
|
ifMatch: "\"abc123\"",
|
|
missingCode: s3err.ErrPreconditionFailed,
|
|
expected: s3err.ErrNone,
|
|
},
|
|
{
|
|
name: "wildcard succeeds for existing entry",
|
|
entry: existingEntry,
|
|
ifMatch: "*",
|
|
missingCode: s3err.ErrPreconditionFailed,
|
|
expected: s3err.ErrNone,
|
|
},
|
|
{
|
|
name: "mismatched etag fails",
|
|
entry: existingEntry,
|
|
ifMatch: "\"other\"",
|
|
missingCode: s3err.ErrPreconditionFailed,
|
|
expected: s3err.ErrPreconditionFailed,
|
|
},
|
|
{
|
|
name: "missing current object fails single delete",
|
|
entry: nil,
|
|
ifMatch: "*",
|
|
missingCode: s3err.ErrPreconditionFailed,
|
|
expected: s3err.ErrPreconditionFailed,
|
|
},
|
|
{
|
|
name: "missing current object returns no such key for batch delete",
|
|
entry: nil,
|
|
ifMatch: "*",
|
|
missingCode: s3err.ErrNoSuchKey,
|
|
expected: s3err.ErrNoSuchKey,
|
|
},
|
|
{
|
|
name: "current delete marker behaves like missing object",
|
|
entry: normalizeConditionalTargetEntry(deleteMarkerEntry),
|
|
ifMatch: "*",
|
|
missingCode: s3err.ErrPreconditionFailed,
|
|
expected: s3err.ErrPreconditionFailed,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if errCode := s3a.validateDeleteIfMatch(tc.entry, tc.ifMatch, tc.missingCode); errCode != tc.expected {
|
|
t.Fatalf("validateDeleteIfMatch() = %v, want %v", errCode, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteObjectsRequestUnmarshalConditionalETags(t *testing.T) {
|
|
var req DeleteObjectsRequest
|
|
body := []byte(`
|
|
<Delete>
|
|
<Quiet>true</Quiet>
|
|
<Object>
|
|
<Key>first.txt</Key>
|
|
<ETag>*</ETag>
|
|
</Object>
|
|
<Object>
|
|
<Key>second.txt</Key>
|
|
<VersionId>3HL4kqCxf3vjVBH40Nrjfkd</VersionId>
|
|
<ETag>"abc123"</ETag>
|
|
</Object>
|
|
</Delete>`)
|
|
|
|
if err := xml.Unmarshal(body, &req); err != nil {
|
|
t.Fatalf("xml.Unmarshal() error = %v", err)
|
|
}
|
|
if !req.Quiet {
|
|
t.Fatalf("expected Quiet=true")
|
|
}
|
|
if len(req.Objects) != 2 {
|
|
t.Fatalf("expected 2 objects, got %d", len(req.Objects))
|
|
}
|
|
if req.Objects[0].ETag != "*" {
|
|
t.Fatalf("expected first object ETag to be '*', got %q", req.Objects[0].ETag)
|
|
}
|
|
if req.Objects[1].ETag != "\"abc123\"" {
|
|
t.Fatalf("expected second object ETag to preserve quotes, got %q", req.Objects[1].ETag)
|
|
}
|
|
if req.Objects[1].VersionId != "3HL4kqCxf3vjVBH40Nrjfkd" {
|
|
t.Fatalf("expected second object VersionId to unmarshal, got %q", req.Objects[1].VersionId)
|
|
}
|
|
}
|