Files
seaweedfs/weed/s3api/s3tables/handler_update_conflict_test.go
T
4850def831 fix(s3tables): bind table and view writes to the record they were authorized against (#12328)
* s3tables: bind table and view writes to the record they were authorized against

* s3tables: bind UpdateNamespace to the namespace it was authorized against

UpdateNamespace writes back the whole record it read, owner and creation
time included. A namespace deleted and created again before the write took
the first one's owner and CreatedAt, which also undoes the CreatedAt binding
creates now rely on. Refuse the write with 409 when the namespace at the
path has another CreatedAt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 02:50:45 -08:00

90 lines
3.3 KiB
Go

package s3tables
import (
"fmt"
"net/http"
"testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// claimRecord puts a delete claim on an existing catalog entry, the state a
// record is in while another operation owns it.
func claimRecord(t *testing.T, fs *s3tablestest.MemFiler, dir, name string) {
t.Helper()
entry := fs.Get(dir, name)
require.NotNil(t, entry)
claimed := map[string][]byte{}
for k, v := range entry.Extended {
claimed[k] = v
}
claimed[ExtendedKeyDeleting] = []byte("delete")
fs.Put(dir, name, claimed)
}
// A write landing on a record another operation claimed is an expected
// conflict, not a server fault: the caller must see 409.
func TestUpdateViewClaimedRecordConflicts(t *testing.T) {
fs, m := startRenameManager(t)
seedView(t, fs, "v")
claimRecord(t, fs, GetNamespacePath(renameTestBucket, "ns"), "v")
status, resp := runHandler(t, m, fs.Client, "UpdateView", &UpdateViewRequest{
TableBucketARN: mustBucketARN(t),
Namespace: []string{"ns"},
Name: "v",
MetadataLocation: "s3://" + renameTestBucket + "/ns/v/metadata/v2.metadata.json",
})
assert.Equal(t, http.StatusConflict, status, "%+v", resp)
assert.Equal(t, ErrCodeConflict, resp.Type)
}
func TestTagResourceClaimedRecordConflicts(t *testing.T) {
fs, m := startRenameManager(t)
claimRecord(t, fs, GetNamespacePath(renameTestBucket, "ns"), "t")
tableARN := fmt.Sprintf("arn:aws:s3tables:%s:%s:bucket/%s/table/ns/t", DefaultRegion, DefaultAccountID, renameTestBucket)
status, resp := runHandler(t, m, fs.Client, "TagResource", &TagResourceRequest{
ResourceARN: tableARN,
Tags: map[string]string{"k": "v"},
})
assert.Equal(t, http.StatusConflict, status, "%+v", resp)
assert.Equal(t, ErrCodeConflict, resp.Type)
}
func TestUntagResourceClaimedRecordConflicts(t *testing.T) {
fs, m := startRenameManager(t)
claimRecord(t, fs, GetNamespacePath(renameTestBucket, "ns"), "t")
tableARN := fmt.Sprintf("arn:aws:s3tables:%s:%s:bucket/%s/table/ns/t", DefaultRegion, DefaultAccountID, renameTestBucket)
status, resp := runHandler(t, m, fs.Client, "UntagResource", &UntagResourceRequest{
ResourceARN: tableARN,
TagKeys: []string{"k"},
})
assert.Equal(t, http.StatusConflict, status, "%+v", resp)
assert.Equal(t, ErrCodeConflict, resp.Type)
}
// DeleteTablePolicy writes like every other mutation of a table record, so a
// record another operation claimed refuses it with 409 instead of losing the
// policy under the claim's owner.
func TestDeleteTablePolicyClaimedRecordConflicts(t *testing.T) {
fs, m := startTableT(t, map[string][]byte{ExtendedKeyPolicy: []byte(bindTestPolicy)})
nsPath := GetNamespacePath(renameTestBucket, "ns")
claimRecord(t, fs, nsPath, "t")
status, resp := runHandler(t, m, fs.Client, "DeleteTablePolicy", &DeleteTablePolicyRequest{
TableBucketARN: mustBucketARN(t),
Namespace: []string{"ns"},
Name: "t",
})
assert.Equal(t, http.StatusConflict, status, "%+v", resp)
assert.Equal(t, ErrCodeConflict, resp.Type, "%+v", resp)
extended := fs.Get(nsPath, "t").Extended
assert.Equal(t, []byte(bindTestPolicy), extended[ExtendedKeyPolicy], "the refused delete keeps the policy")
assert.Equal(t, []byte("delete"), extended[ExtendedKeyDeleting], "and leaves the claim to its owner")
}