Files
at-container-registry/pkg/hold/pds/crew_test.go
T
Evan JarrettandClaude Fable 5.1 61a934debb hold: look crew members up by rkey instead of walking the collection
ValidateBlobWriteAccess, ValidateBlobReadAccess, ValidateOwnerOrCrewAdmin
and getCrewTier each listed every crew record to find one member: open a
carstore session, walk the MST, CBOR-decode each record, compare DIDs.
That ran on every multipart call from the appview, including the part
URL request for every 10MB, and on every getBlob presign, so on a hold
with hundreds of crew each part cost hundreds of decodes.

lookupCrewMember tries the deterministic rkey first (one record read)
and only falls back to the walk on a not-found miss. The fallback is
required: records created before the hash-rkey scheme sit at a TID rkey,
and the boot-time migration that rekeyed them only existed between
e0a2dda and b2d6842, so a hold that upgraded across that window still
has them. Members hit the O(1) path; only genuine non-members pay for
the walk, and they are denied anyway.

Every authorization decision and error string is unchanged. Tests cover
the deterministic hit, a legacy TID-keyed member found only through the
fallback, a non-member, and a storage error surfacing as an error rather
than a silent denial.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yf1ZVA7sXYhQNb9tCo1m5
2026-09-09 09:31:16 -05:00

714 lines
19 KiB
Go

package pds
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"atcr.io/pkg/atproto"
)
// TestAddCrewMember tests adding a single crew member
func TestAddCrewMember(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:alice123"
role := "writer"
permissions := []string{"blob:read", "blob:write"}
recordCID, err := pds.AddCrewMember(ctx, memberDID, role, permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// Verify CID is defined
if !recordCID.Defined() {
t.Error("Expected defined CID")
}
// List crew members to verify
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
crew := crewMembers[0]
if crew.Record.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, crew.Record.Member)
}
if crew.Record.Role != role {
t.Errorf("Expected role %s, got %s", role, crew.Record.Role)
}
if len(crew.Record.Permissions) != len(permissions) {
t.Fatalf("Expected %d permissions, got %d", len(permissions), len(crew.Record.Permissions))
}
for i, perm := range permissions {
if crew.Record.Permissions[i] != perm {
t.Errorf("Expected permission[%d]=%s, got %s", i, perm, crew.Record.Permissions[i])
}
}
if crew.Record.Type != atproto.CrewCollection {
t.Errorf("Expected type %s, got %s", atproto.CrewCollection, crew.Record.Type)
}
if crew.Record.AddedAt == "" {
t.Error("Expected addedAt to be set")
}
}
// TestGetCrewMember tests retrieving a crew member by rkey
func TestGetCrewMember(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:bob456"
role := "reader"
permissions := []string{"blob:read"}
_, err := pds.AddCrewMember(ctx, memberDID, role, permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// List to get the rkey
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) == 0 {
t.Fatal("Expected at least one crew member")
}
rkey := crewMembers[0].Rkey
// Get crew member by rkey
retrievedCID, crew, err := pds.GetCrewMember(ctx, rkey)
if err != nil {
t.Fatalf("GetCrewMember failed: %v", err)
}
// Verify CID matches
if !crewMembers[0].Cid.Equals(retrievedCID) {
t.Error("Expected retrieved CID to match")
}
// Verify crew data
if crew.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, crew.Member)
}
if crew.Role != role {
t.Errorf("Expected role %s, got %s", role, crew.Role)
}
if len(crew.Permissions) != len(permissions) {
t.Fatalf("Expected %d permissions, got %d", len(permissions), len(crew.Permissions))
}
}
// TestGetCrewMember_NotFound tests error handling for missing crew
func TestGetCrewMember_NotFound(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Try to get non-existent crew member
_, _, err := pds.GetCrewMember(ctx, "nonexistent-rkey")
if err == nil {
t.Fatal("Expected error when getting non-existent crew member")
}
// A missing record must be distinguishable from a storage failure. Callers
// that treat "not crew" as a benign no-op (updateCrewTier) would otherwise
// swallow a CAR-store error as a non-member and silently drop a tier grant.
if !errors.Is(err, ErrCrewMemberNotFound) {
t.Errorf("Expected ErrCrewMemberNotFound, got: %v", err)
}
if !strings.Contains(err.Error(), "nonexistent-rkey") {
t.Errorf("Expected the rkey in the error, got: %s", err)
}
}
// TestListCrewMembers_Empty tests listing when no crew exists
func TestListCrewMembers_Empty(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// List crew members (should be empty)
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 0 {
t.Errorf("Expected 0 crew members, got %d", len(crewMembers))
}
}
// TestListCrewMembers_Multiple tests listing with multiple crew members
func TestListCrewMembers_Multiple(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add multiple crew members
members := []struct {
did string
role string
permissions []string
}{
{
did: "did:plc:alice123",
role: "admin",
permissions: []string{"blob:read", "blob:write", "crew:admin"},
},
{
did: "did:plc:bob456",
role: "writer",
permissions: []string{"blob:read", "blob:write"},
},
{
did: "did:plc:charlie789",
role: "reader",
permissions: []string{"blob:read"},
},
}
for _, m := range members {
_, err := pds.AddCrewMember(ctx, m.did, m.role, m.permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed for %s: %v", m.did, err)
}
}
// List all crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != len(members) {
t.Fatalf("Expected %d crew members, got %d", len(members), len(crewMembers))
}
// Verify each crew member (order may vary, so check by DID)
foundMembers := make(map[string]*CrewMemberWithKey)
for _, cm := range crewMembers {
foundMembers[cm.Record.Member] = cm
}
for _, m := range members {
crew, found := foundMembers[m.did]
if !found {
t.Errorf("Expected to find crew member %s", m.did)
continue
}
if crew.Record.Role != m.role {
t.Errorf("Expected role %s for %s, got %s", m.role, m.did, crew.Record.Role)
}
if len(crew.Record.Permissions) != len(m.permissions) {
t.Errorf("Expected %d permissions for %s, got %d", len(m.permissions), m.did, len(crew.Record.Permissions))
}
// Verify rkey is set
if crew.Rkey == "" {
t.Errorf("Expected non-empty rkey for %s", m.did)
}
// Verify CID is defined
if !crew.Cid.Defined() {
t.Errorf("Expected defined CID for %s", m.did)
}
}
}
// TestRemoveCrewMember tests deleting a crew member
func TestRemoveCrewMember(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:alice123"
_, err := pds.AddCrewMember(ctx, memberDID, "writer", []string{"blob:read", "blob:write"}, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// List to get the rkey
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
rkey := crewMembers[0].Rkey
// Remove crew member
err = pds.RemoveCrewMember(ctx, rkey)
if err != nil {
t.Fatalf("RemoveCrewMember failed: %v", err)
}
// Verify crew member is gone
crewMembers, err = pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed after removal: %v", err)
}
if len(crewMembers) != 0 {
t.Errorf("Expected 0 crew members after removal, got %d", len(crewMembers))
}
// Try to get removed crew member (should fail)
_, _, err = pds.GetCrewMember(ctx, rkey)
if err == nil {
t.Error("Expected error when getting removed crew member")
}
}
// TestRemoveCrewMember_NotFound tests removing non-existent crew member
func TestRemoveCrewMember_NotFound(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Try to remove non-existent crew member
err := pds.RemoveCrewMember(ctx, "nonexistent-rkey")
if err == nil {
t.Fatal("Expected error when removing non-existent crew member")
}
// Verify error message
errMsg := err.Error()
if !strings.Contains(errMsg, "failed to delete crew record") {
t.Errorf("Expected 'failed to delete crew record' in error, got: %s", errMsg)
}
}
// TestRemoveCrewMember_Multiple tests removing one crew member from many
func TestRemoveCrewMember_Multiple(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add multiple crew members
dids := []string{
"did:plc:alice123",
"did:plc:bob456",
"did:plc:charlie789",
}
for _, did := range dids {
_, err := pds.AddCrewMember(ctx, did, "writer", []string{"blob:read"}, "")
if err != nil {
t.Fatalf("AddCrewMember failed for %s: %v", did, err)
}
}
// List crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 3 {
t.Fatalf("Expected 3 crew members, got %d", len(crewMembers))
}
// Remove middle member
middleRkey := crewMembers[1].Rkey
middleDID := crewMembers[1].Record.Member
err = pds.RemoveCrewMember(ctx, middleRkey)
if err != nil {
t.Fatalf("RemoveCrewMember failed: %v", err)
}
// Verify only 2 remain
crewMembers, err = pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed after removal: %v", err)
}
if len(crewMembers) != 2 {
t.Fatalf("Expected 2 crew members after removal, got %d", len(crewMembers))
}
// Verify removed member is not in list
for _, cm := range crewMembers {
if cm.Record.Member == middleDID {
t.Errorf("Expected %s to be removed, but still found in list", middleDID)
}
}
}
// TestCrewRecord_CBORRoundtrip tests CBOR marshal/unmarshal integrity
func TestCrewRecord_CBORRoundtrip(t *testing.T) {
tests := []struct {
name string
record *atproto.CrewRecord
}{
{
name: "Basic crew member",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:alice123",
Role: "writer",
Permissions: []string{"blob:read", "blob:write"},
AddedAt: "2025-10-16T12:00:00Z",
},
},
{
name: "Admin crew member",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:bob456",
Role: "admin",
Permissions: []string{"blob:read", "blob:write", "crew:admin"},
AddedAt: "2025-10-16T13:00:00Z",
},
},
{
name: "Reader crew member",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:charlie789",
Role: "reader",
Permissions: []string{"blob:read"},
AddedAt: "2025-10-16T14:00:00Z",
},
},
{
name: "Crew member with empty permissions",
record: &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: "did:plc:dave012",
Role: "none",
Permissions: []string{},
AddedAt: "2025-10-16T15:00:00Z",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Marshal to CBOR
var buf bytes.Buffer
err := tt.record.MarshalCBOR(&buf)
if err != nil {
t.Fatalf("MarshalCBOR failed: %v", err)
}
cborBytes := buf.Bytes()
if len(cborBytes) == 0 {
t.Fatal("Expected non-empty CBOR bytes")
}
// Unmarshal from CBOR
var decoded atproto.CrewRecord
err = decoded.UnmarshalCBOR(bytes.NewReader(cborBytes))
if err != nil {
t.Fatalf("UnmarshalCBOR failed: %v", err)
}
// Verify all fields match
if decoded.Type != tt.record.Type {
t.Errorf("Type mismatch: expected %s, got %s", tt.record.Type, decoded.Type)
}
if decoded.Member != tt.record.Member {
t.Errorf("Member mismatch: expected %s, got %s", tt.record.Member, decoded.Member)
}
if decoded.Role != tt.record.Role {
t.Errorf("Role mismatch: expected %s, got %s", tt.record.Role, decoded.Role)
}
if decoded.AddedAt != tt.record.AddedAt {
t.Errorf("AddedAt mismatch: expected %s, got %s", tt.record.AddedAt, decoded.AddedAt)
}
// Verify permissions
if len(decoded.Permissions) != len(tt.record.Permissions) {
t.Fatalf("Permissions length mismatch: expected %d, got %d", len(tt.record.Permissions), len(decoded.Permissions))
}
for i, perm := range tt.record.Permissions {
if decoded.Permissions[i] != perm {
t.Errorf("Permission[%d] mismatch: expected %s, got %s", i, perm, decoded.Permissions[i])
}
}
})
}
}
// TestCrewMemberWithKey_Structure tests the CrewMemberWithKey struct
func TestCrewMemberWithKey_Structure(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member
memberDID := "did:plc:alice123"
_, err := pds.AddCrewMember(ctx, memberDID, "writer", []string{"blob:read"}, "")
if err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
// List crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
cm := crewMembers[0]
// Verify CrewMemberWithKey structure
if cm.Rkey == "" {
t.Error("Expected non-empty Rkey")
}
if !cm.Cid.Defined() {
t.Error("Expected defined Cid")
}
if cm.Record == nil {
t.Fatal("Expected non-nil Record")
}
if cm.Record.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, cm.Record.Member)
}
}
// TestAddCrewMember_DidWeb tests adding crew members with did:web DIDs
func TestAddCrewMember_DidWeb(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew member with did:web
memberDID := "did:web:alice.example.com"
role := "writer"
permissions := []string{"blob:read", "blob:write"}
recordCID, err := pds.AddCrewMember(ctx, memberDID, role, permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed with did:web: %v", err)
}
if !recordCID.Defined() {
t.Error("Expected defined CID")
}
// Verify crew member was added
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != 1 {
t.Fatalf("Expected 1 crew member, got %d", len(crewMembers))
}
crew := crewMembers[0]
if crew.Record.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, crew.Record.Member)
}
// Verify we can get it by rkey
_, retrievedCrew, err := pds.GetCrewMember(ctx, crew.Rkey)
if err != nil {
t.Fatalf("GetCrewMember failed for did:web: %v", err)
}
if retrievedCrew.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, retrievedCrew.Member)
}
}
// TestListCrewMembers_MixedDIDs tests listing crew members with mixed DID types
func TestListCrewMembers_MixedDIDs(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
// Add crew members with different DID types
members := []struct {
did string
role string
permissions []string
}{
{
did: "did:plc:alice123",
role: "admin",
permissions: []string{"blob:read", "blob:write", "crew:admin"},
},
{
did: "did:web:bob.example.com",
role: "writer",
permissions: []string{"blob:read", "blob:write"},
},
{
did: "did:web:charlie.example.org",
role: "reader",
permissions: []string{"blob:read"},
},
}
for _, m := range members {
_, err := pds.AddCrewMember(ctx, m.did, m.role, m.permissions, "")
if err != nil {
t.Fatalf("AddCrewMember failed for %s: %v", m.did, err)
}
}
// List all crew members
crewMembers, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed: %v", err)
}
if len(crewMembers) != len(members) {
t.Fatalf("Expected %d crew members, got %d", len(members), len(crewMembers))
}
// Verify each crew member exists (order may vary)
foundMembers := make(map[string]*CrewMemberWithKey)
for _, cm := range crewMembers {
foundMembers[cm.Record.Member] = cm
}
for _, m := range members {
crew, found := foundMembers[m.did]
if !found {
t.Errorf("Expected to find crew member %s", m.did)
continue
}
if crew.Record.Role != m.role {
t.Errorf("Expected role %s for %s, got %s", m.role, m.did, crew.Record.Role)
}
}
}
// TestLookupCrewMember_DeterministicRkey verifies the O(1) fast path finds a
// crew member written by AddCrewMember.
func TestLookupCrewMember_DeterministicRkey(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
memberDID := "did:plc:fastpath"
if _, err := pds.AddCrewMember(ctx, memberDID, "writer", []string{"blob:write"}, "bosun"); err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
record, isCrew, err := pds.lookupCrewMember(ctx, memberDID)
if err != nil {
t.Fatalf("lookupCrewMember returned error: %v", err)
}
if !isCrew {
t.Fatal("Expected member to be found")
}
if record.Member != memberDID {
t.Errorf("Expected member %s, got %s", memberDID, record.Member)
}
if record.Tier != "bosun" {
t.Errorf("Expected tier bosun, got %s", record.Tier)
}
}
// TestLookupCrewMember_LegacyTIDRkey verifies the fallback walk still finds a
// crew record left at a TID rkey by a pre-migration hold. Without the fallback
// these members would silently lose authorization.
func TestLookupCrewMember_LegacyTIDRkey(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
memberDID := "did:plc:legacytid"
// Write the record the way pre-2026-01-06 holds did: auto-generated TID rkey.
legacy := &atproto.CrewRecord{
Type: atproto.CrewCollection,
Member: memberDID,
Role: "writer",
Permissions: []string{"blob:write"},
Tier: "deckhand",
}
rkey, _, err := pds.repomgr.CreateRecord(ctx, pds.uid, atproto.CrewCollection, legacy)
if err != nil {
t.Fatalf("CreateRecord failed: %v", err)
}
if rkey == atproto.CrewRecordKey(memberDID) {
t.Fatalf("Test setup is wrong: CreateRecord produced the deterministic rkey %s", rkey)
}
// The O(1) read must miss, proving the fallback is what found the record.
if _, _, err := pds.GetCrewMemberByDID(ctx, memberDID); !errors.Is(err, ErrCrewMemberNotFound) {
t.Fatalf("Expected ErrCrewMemberNotFound from deterministic lookup, got %v", err)
}
record, isCrew, err := pds.lookupCrewMember(ctx, memberDID)
if err != nil {
t.Fatalf("lookupCrewMember returned error: %v", err)
}
if !isCrew {
t.Fatal("Expected legacy TID-keyed member to be found via fallback walk")
}
if record.Tier != "deckhand" {
t.Errorf("Expected tier deckhand, got %s", record.Tier)
}
}
// TestLookupCrewMember_NotFound verifies a genuine non-member reports
// (nil, false, nil) rather than an error, so callers deny with an AuthError.
func TestLookupCrewMember_NotFound(t *testing.T) {
pds, ctx := setupTestPDS(t)
defer pds.Close()
if _, err := pds.AddCrewMember(ctx, "did:plc:someone", "writer", []string{"blob:write"}, ""); err != nil {
t.Fatalf("AddCrewMember failed: %v", err)
}
record, isCrew, err := pds.lookupCrewMember(ctx, "did:plc:stranger")
if err != nil {
t.Fatalf("Expected nil error for a non-member, got %v", err)
}
if isCrew {
t.Error("Expected non-member to report isCrew=false")
}
if record != nil {
t.Errorf("Expected nil record for a non-member, got %+v", record)
}
}
// TestLookupCrewMember_StorageError verifies a real storage failure surfaces as
// an error instead of being mistaken for "not a crew member", which would turn
// a broken CAR store into a silent authorization denial.
func TestLookupCrewMember_StorageError(t *testing.T) {
ctx := context.Background()
keyPath := filepath.Join(t.TempDir(), "signing-key")
if err := os.WriteFile(keyPath, sharedTestKey, 0600); err != nil {
t.Fatalf("Failed to write signing key: %v", err)
}
// Deliberately skip InitNewActor, so the repo has no head to read.
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", ":memory:", keyPath, false)
if err != nil {
t.Fatalf("Failed to create test PDS: %v", err)
}
defer pds.Close()
record, isCrew, err := pds.lookupCrewMember(ctx, "did:plc:anyone")
if err == nil {
t.Fatalf("Expected a storage error, got (%+v, %v, nil)", record, isCrew)
}
if isCrew {
t.Error("Expected isCrew=false alongside a storage error")
}
}