mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-26 04:04:15 +00:00
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 betweene0a2ddaandb2d6842, 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
714 lines
19 KiB
Go
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")
|
|
}
|
|
}
|