mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
950 lines
28 KiB
Go
950 lines
28 KiB
Go
package pds
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
)
|
|
|
|
// TestNewHoldPDS_NewRepo tests creating a new hold PDS with fresh database
|
|
func TestNewHoldPDS_NewRepo(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
// Paths for database and key
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
// Create new hold PDS
|
|
did := "did:web:hold.example.com"
|
|
publicURL := "https://hold.example.com"
|
|
|
|
pds, err := NewHoldPDS(ctx, did, publicURL, "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Verify DID was set
|
|
if pds.DID() != did {
|
|
t.Errorf("Expected DID %s, got %s", did, pds.DID())
|
|
}
|
|
|
|
// Verify signing key was created
|
|
if pds.SigningKey() == nil {
|
|
t.Error("Expected signing key to be created")
|
|
}
|
|
|
|
// Verify key file exists
|
|
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
|
|
t.Error("Expected signing key file to be created")
|
|
}
|
|
|
|
// Verify database file exists (SQLite creates db.sqlite3 inside the directory)
|
|
dbFile := filepath.Join(dbPath, "db.sqlite3")
|
|
if _, err := os.Stat(dbFile); os.IsNotExist(err) {
|
|
t.Errorf("Expected database file to be created at %s", dbFile)
|
|
}
|
|
}
|
|
|
|
// TestNewHoldPDS_ExistingRepo tests opening an existing hold PDS database
|
|
func TestNewHoldPDS_ExistingRepo(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
did := "did:web:hold.example.com"
|
|
publicURL := "https://hold.example.com"
|
|
|
|
// Create first PDS instance and bootstrap it
|
|
pds1, err := NewHoldPDS(ctx, did, publicURL, "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("First NewHoldPDS failed: %v", err)
|
|
}
|
|
|
|
// Bootstrap with a captain record
|
|
ownerDID := "did:plc:owner123"
|
|
if err := pds1.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true}); err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Verify captain record exists
|
|
_, captain1, err := pds1.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed after bootstrap: %v", err)
|
|
}
|
|
if captain1.Owner != ownerDID {
|
|
t.Errorf("Expected owner %s, got %s", ownerDID, captain1.Owner)
|
|
}
|
|
|
|
// Close first instance
|
|
pds1.Close()
|
|
|
|
// Re-open the same database
|
|
pds2, err := NewHoldPDS(ctx, did, publicURL, "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("Second NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds2.Close()
|
|
|
|
// Verify captain record still exists
|
|
_, captain2, err := pds2.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed after reopening: %v", err)
|
|
}
|
|
|
|
// Verify captain data persisted
|
|
if captain2.Owner != ownerDID {
|
|
t.Errorf("Expected owner %s after reopen, got %s", ownerDID, captain2.Owner)
|
|
}
|
|
if !captain2.Public {
|
|
t.Error("Expected captain.Public to be true")
|
|
}
|
|
if captain2.AllowAllCrew {
|
|
t.Error("Expected captain.AllowAllCrew to be false")
|
|
}
|
|
}
|
|
|
|
// TestBootstrap_NewRepo tests bootstrap on a new repository
|
|
func TestBootstrap_NewRepo(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Bootstrap with owner
|
|
ownerDID := "did:plc:alice123"
|
|
publicAccess := true
|
|
allowAllCrew := false
|
|
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: publicAccess, AllowAllCrew: allowAllCrew})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Verify captain record was created
|
|
_, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify captain fields
|
|
if captain.Owner != ownerDID {
|
|
t.Errorf("Expected owner %s, got %s", ownerDID, captain.Owner)
|
|
}
|
|
if captain.Public != publicAccess {
|
|
t.Errorf("Expected public=%v, got %v", publicAccess, captain.Public)
|
|
}
|
|
if captain.AllowAllCrew != allowAllCrew {
|
|
t.Errorf("Expected allowAllCrew=%v, got %v", allowAllCrew, captain.AllowAllCrew)
|
|
}
|
|
if captain.Type != atproto.CaptainCollection {
|
|
t.Errorf("Expected type %s, got %s", atproto.CaptainCollection, captain.Type)
|
|
}
|
|
if captain.DeployedAt == "" {
|
|
t.Error("Expected deployedAt to be set")
|
|
}
|
|
|
|
// Verify owner was added as crew member
|
|
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 != ownerDID {
|
|
t.Errorf("Expected crew member %s, got %s", ownerDID, crew.Record.Member)
|
|
}
|
|
if crew.Record.Role != "admin" {
|
|
t.Errorf("Expected role admin, got %s", crew.Record.Role)
|
|
}
|
|
|
|
// Verify permissions
|
|
expectedPerms := []string{"blob:read", "blob:write", "crew:admin"}
|
|
if len(crew.Record.Permissions) != len(expectedPerms) {
|
|
t.Fatalf("Expected %d permissions, got %d", len(expectedPerms), len(crew.Record.Permissions))
|
|
}
|
|
for i, perm := range expectedPerms {
|
|
if crew.Record.Permissions[i] != perm {
|
|
t.Errorf("Expected permission[%d]=%s, got %s", i, perm, crew.Record.Permissions[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBootstrap_Idempotent tests that bootstrap is idempotent
|
|
func TestBootstrap_Idempotent(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
ownerDID := "did:plc:alice123"
|
|
|
|
// First bootstrap
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true})
|
|
if err != nil {
|
|
t.Fatalf("First bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Get captain CID after first bootstrap
|
|
cid1, captain1, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Get crew count after first bootstrap
|
|
crew1, err := pds.ListCrewMembers(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCrewMembers failed: %v", err)
|
|
}
|
|
crewCount1 := len(crew1)
|
|
|
|
// Second bootstrap (should be idempotent - skip creation)
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true})
|
|
if err != nil {
|
|
t.Fatalf("Second bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Verify captain record unchanged
|
|
cid2, captain2, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed after second bootstrap: %v", err)
|
|
}
|
|
|
|
if !cid1.Equals(cid2) {
|
|
t.Error("Expected captain CID to remain unchanged after second bootstrap")
|
|
}
|
|
if captain1.Owner != captain2.Owner {
|
|
t.Error("Expected captain owner to remain unchanged")
|
|
}
|
|
|
|
// Verify crew count unchanged (owner not added twice)
|
|
crew2, err := pds.ListCrewMembers(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCrewMembers failed after second bootstrap: %v", err)
|
|
}
|
|
crewCount2 := len(crew2)
|
|
|
|
if crewCount1 != crewCount2 {
|
|
t.Errorf("Expected crew count to remain %d, got %d (owner may have been added twice)", crewCount1, crewCount2)
|
|
}
|
|
}
|
|
|
|
// TestBootstrap_EmptyOwner tests that bootstrap with empty owner is a no-op
|
|
func TestBootstrap_EmptyOwner(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Bootstrap with empty owner DID (should be no-op)
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{Public: true})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap with empty owner should not error: %v", err)
|
|
}
|
|
|
|
// Verify captain record was NOT created
|
|
_, _, err = pds.GetCaptainRecord(ctx)
|
|
if err == nil {
|
|
t.Error("Expected GetCaptainRecord to fail (no captain record), but it succeeded")
|
|
}
|
|
// Verify it's a "not found" type error
|
|
if err != nil && !strings.Contains(err.Error(), "not found") && !strings.Contains(err.Error(), "failed to get captain record") {
|
|
t.Errorf("Expected 'not found' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLexiconTypeRegistration tests that captain and crew types are registered
|
|
func TestLexiconTypeRegistration(t *testing.T) {
|
|
// The init() function in server.go registers types
|
|
// We can verify this by creating a PDS and doing a round-trip write/read
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Bootstrap to create captain record
|
|
ownerDID := "did:plc:alice123"
|
|
if err := pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true}); err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// GetCaptainRecord uses type assertion to *atproto.CaptainRecord
|
|
// If the type wasn't registered, this would fail with type assertion error
|
|
_, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify we got the correct concrete type
|
|
if captain == nil {
|
|
t.Fatal("Expected non-nil captain record")
|
|
}
|
|
if captain.Type != atproto.CaptainCollection {
|
|
t.Errorf("Expected captain type %s, got %s", atproto.CaptainCollection, captain.Type)
|
|
}
|
|
|
|
// Do the same for crew record
|
|
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")
|
|
}
|
|
|
|
crew := crewMembers[0].Record
|
|
if crew.Type != atproto.CrewCollection {
|
|
t.Errorf("Expected crew type %s, got %s", atproto.CrewCollection, crew.Type)
|
|
}
|
|
}
|
|
|
|
// TestBootstrap_DidWebOwner tests bootstrap with did:web owner
|
|
func TestBootstrap_DidWebOwner(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold01.atcr.io", "https://hold01.atcr.io", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Bootstrap with did:web owner (not did:plc)
|
|
ownerDID := "did:web:alice.example.com"
|
|
publicAccess := true
|
|
allowAllCrew := false
|
|
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: publicAccess, AllowAllCrew: allowAllCrew})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap failed with did:web owner: %v", err)
|
|
}
|
|
|
|
// Verify captain record was created with did:web owner
|
|
_, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify captain fields
|
|
if captain.Owner != ownerDID {
|
|
t.Errorf("Expected owner %s, got %s", ownerDID, captain.Owner)
|
|
}
|
|
if captain.Public != publicAccess {
|
|
t.Errorf("Expected public=%v, got %v", publicAccess, captain.Public)
|
|
}
|
|
if captain.AllowAllCrew != allowAllCrew {
|
|
t.Errorf("Expected allowAllCrew=%v, got %v", allowAllCrew, captain.AllowAllCrew)
|
|
}
|
|
|
|
// Verify owner was added as crew member
|
|
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 != ownerDID {
|
|
t.Errorf("Expected crew member %s, got %s", ownerDID, crew.Record.Member)
|
|
}
|
|
if crew.Record.Role != "admin" {
|
|
t.Errorf("Expected role admin, got %s", crew.Record.Role)
|
|
}
|
|
}
|
|
|
|
// TestBootstrap_MixedDIDs tests bootstrap with mixed DID types
|
|
func TestBootstrap_MixedDIDs(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
// Create hold with did:web
|
|
holdDID := "did:web:hold.example.com"
|
|
pds, err := NewHoldPDS(ctx, holdDID, "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Bootstrap with did:plc owner
|
|
plcOwner := "did:plc:alice123"
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: plcOwner, Public: true})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Add did:web crew member
|
|
webMember := "did:web:bob.example.com"
|
|
_, err = pds.AddCrewMember(ctx, webMember, "writer", []string{"blob:read", "blob:write"}, "")
|
|
if err != nil {
|
|
t.Fatalf("AddCrewMember failed with did:web: %v", err)
|
|
}
|
|
|
|
// Verify captain
|
|
_, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
if captain.Owner != plcOwner {
|
|
t.Errorf("Expected captain owner %s, got %s", plcOwner, captain.Owner)
|
|
}
|
|
|
|
// Verify crew members (should have both did:plc and did:web)
|
|
crewMembers, err := pds.ListCrewMembers(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCrewMembers failed: %v", err)
|
|
}
|
|
|
|
if len(crewMembers) != 2 {
|
|
t.Fatalf("Expected 2 crew members, got %d", len(crewMembers))
|
|
}
|
|
|
|
// Verify both DIDs are present
|
|
foundPLC := false
|
|
foundWeb := false
|
|
for _, cm := range crewMembers {
|
|
if cm.Record.Member == plcOwner {
|
|
foundPLC = true
|
|
}
|
|
if cm.Record.Member == webMember {
|
|
foundWeb = true
|
|
}
|
|
}
|
|
|
|
if !foundPLC {
|
|
t.Errorf("Expected to find did:plc member %s", plcOwner)
|
|
}
|
|
if !foundWeb {
|
|
t.Errorf("Expected to find did:web member %s", webMember)
|
|
}
|
|
}
|
|
|
|
// TestBootstrap_CrewWithoutCaptain tests bootstrap when crew exists but captain doesn't
|
|
// This edge case could happen if repo state is corrupted or partially initialized
|
|
func TestBootstrap_CrewWithoutCaptain(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Initialize repo manually
|
|
err = pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("InitNewActor failed: %v", err)
|
|
}
|
|
|
|
// Create crew member WITHOUT captain (unusual state)
|
|
ownerDID := "did:plc:alice123"
|
|
_, err = pds.AddCrewMember(ctx, ownerDID, "admin", []string{"blob:read", "blob:write", "crew:admin"}, "")
|
|
if err != nil {
|
|
t.Fatalf("AddCrewMember failed: %v", err)
|
|
}
|
|
|
|
// Verify crew exists
|
|
crewBefore, err := pds.ListCrewMembers(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCrewMembers failed: %v", err)
|
|
}
|
|
if len(crewBefore) != 1 {
|
|
t.Fatalf("Expected 1 crew member before bootstrap, got %d", len(crewBefore))
|
|
}
|
|
|
|
// Verify captain doesn't exist
|
|
_, _, err = pds.GetCaptainRecord(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected captain record to not exist before bootstrap")
|
|
}
|
|
|
|
// Bootstrap should create captain record
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Verify captain was created
|
|
_, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed after bootstrap: %v", err)
|
|
}
|
|
if captain.Owner != ownerDID {
|
|
t.Errorf("Expected captain owner %s, got %s", ownerDID, captain.Owner)
|
|
}
|
|
|
|
// Verify crew wasn't duplicated (Bootstrap adds owner as crew, but they already exist)
|
|
// With hash-based rkeys, AddCrewMember uses PutRecord which upserts - no duplicates possible
|
|
crewAfter, err := pds.ListCrewMembers(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCrewMembers failed after bootstrap: %v", err)
|
|
}
|
|
|
|
// Should still have 1 crew member (hash-based rkey ensures upsert, not duplicate)
|
|
if len(crewAfter) != 1 {
|
|
t.Errorf("Expected 1 crew member after bootstrap (upsert), got %d", len(crewAfter))
|
|
}
|
|
}
|
|
|
|
// TestBootstrap_CaptainWithoutCrew tests bootstrap when captain exists but owner crew doesn't
|
|
// This verifies that bootstrap properly adds the owner as crew if missing
|
|
func TestBootstrap_CaptainWithoutCrew(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Initialize repo manually
|
|
err = pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("InitNewActor failed: %v", err)
|
|
}
|
|
|
|
// Create captain record WITHOUT crew (unusual state)
|
|
ownerDID := "did:plc:alice123"
|
|
_, err = pds.CreateCaptainRecord(ctx, ownerDID, true, false, false, "")
|
|
if err != nil {
|
|
t.Fatalf("CreateCaptainRecord failed: %v", err)
|
|
}
|
|
|
|
// Verify captain exists
|
|
_, captain, err := pds.GetCaptainRecord(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetCaptainRecord failed: %v", err)
|
|
}
|
|
if captain.Owner != ownerDID {
|
|
t.Errorf("Expected captain owner %s, got %s", ownerDID, captain.Owner)
|
|
}
|
|
|
|
// Verify crew is empty
|
|
crewBefore, err := pds.ListCrewMembers(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCrewMembers failed: %v", err)
|
|
}
|
|
if len(crewBefore) != 0 {
|
|
t.Fatalf("Expected 0 crew members before bootstrap, got %d", len(crewBefore))
|
|
}
|
|
|
|
// Bootstrap should be idempotent but notice missing crew
|
|
// Currently Bootstrap skips if captain exists, so crew won't be added
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Verify crew after bootstrap
|
|
crewAfter, err := pds.ListCrewMembers(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCrewMembers failed after bootstrap: %v", err)
|
|
}
|
|
|
|
// Bootstrap currently skips everything if captain exists
|
|
// This means crew won't be added in this case
|
|
if len(crewAfter) == 0 {
|
|
t.Logf("Note: Bootstrap skipped adding owner as crew because captain already exists")
|
|
t.Logf("This is current behavior - Bootstrap is fully idempotent and skips if captain exists")
|
|
} else {
|
|
// If we change Bootstrap to be smarter, it might add crew
|
|
t.Logf("Bootstrap added %d crew members", len(crewAfter))
|
|
|
|
// Verify owner was added
|
|
foundOwner := false
|
|
for _, cm := range crewAfter {
|
|
if cm.Record.Member == ownerDID {
|
|
foundOwner = true
|
|
if cm.Record.Role != "admin" {
|
|
t.Errorf("Expected owner role admin, got %s", cm.Record.Role)
|
|
}
|
|
}
|
|
}
|
|
if !foundOwner {
|
|
t.Error("Expected owner to be added as crew member")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests for RecordsIndex feature
|
|
|
|
// TestHoldPDS_RecordsIndex_Nil tests that RecordsIndex is nil for :memory: database
|
|
func TestHoldPDS_RecordsIndex_Nil(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
// Create with :memory: database
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", ":memory:", keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// RecordsIndex should be nil for :memory:
|
|
if pds.RecordsIndex() != nil {
|
|
t.Error("Expected RecordsIndex() to be nil for :memory: database")
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_RecordsIndex_NonNil tests that RecordsIndex is created for file database
|
|
func TestHoldPDS_RecordsIndex_NonNil(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
// Create with file database
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// RecordsIndex should be non-nil for file database
|
|
if pds.RecordsIndex() == nil {
|
|
t.Error("Expected RecordsIndex() to be non-nil for file database")
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_Carstore tests the Carstore getter
|
|
func TestHoldPDS_Carstore(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", ":memory:", keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
if pds.Carstore() == nil {
|
|
t.Error("Expected Carstore() to be non-nil")
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_UID tests the UID getter
|
|
func TestHoldPDS_UID(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", ":memory:", keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// UID should be 1 for single-user PDS
|
|
if pds.UID() != 1 {
|
|
t.Errorf("Expected UID() to be 1, got %d", pds.UID())
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_CreateRecordsIndexEventHandler tests event handler wrapper
|
|
func TestHoldPDS_CreateRecordsIndexEventHandler(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Track if broadcaster was called
|
|
broadcasterCalled := false
|
|
broadcasterHandler := func(ctx context.Context, event *RepoEvent) {
|
|
broadcasterCalled = true
|
|
}
|
|
|
|
// Create handler
|
|
handler := pds.CreateRecordsIndexEventHandler(broadcasterHandler)
|
|
if handler == nil {
|
|
t.Fatal("Expected handler to be non-nil")
|
|
}
|
|
|
|
// Create a test event with create operation
|
|
event := &RepoEvent{
|
|
Ops: []RepoOp{
|
|
{
|
|
Kind: EvtKindCreateRecord,
|
|
Collection: "io.atcr.hold.crew",
|
|
Rkey: "testrkey",
|
|
RecCid: nil, // Will be nil string
|
|
},
|
|
},
|
|
}
|
|
|
|
// Call handler
|
|
handler(ctx, event)
|
|
|
|
// Verify broadcaster was called
|
|
if !broadcasterCalled {
|
|
t.Error("Expected broadcaster handler to be called")
|
|
}
|
|
|
|
// Verify record was indexed
|
|
if pds.RecordsIndex() != nil {
|
|
count, err := pds.RecordsIndex().Count("io.atcr.hold.crew")
|
|
if err != nil {
|
|
t.Fatalf("Count() error = %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("Expected 1 indexed record, got %d", count)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_CreateRecordsIndexEventHandler_Delete tests delete operation
|
|
func TestHoldPDS_CreateRecordsIndexEventHandler_Delete(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
handler := pds.CreateRecordsIndexEventHandler(nil)
|
|
|
|
// First, create a record
|
|
createEvent := &RepoEvent{
|
|
Ops: []RepoOp{
|
|
{
|
|
Kind: EvtKindCreateRecord,
|
|
Collection: "io.atcr.hold.crew",
|
|
Rkey: "testrkey",
|
|
},
|
|
},
|
|
}
|
|
handler(ctx, createEvent)
|
|
|
|
// Verify it was indexed
|
|
count, _ := pds.RecordsIndex().Count("io.atcr.hold.crew")
|
|
if count != 1 {
|
|
t.Fatalf("Expected 1 record after create, got %d", count)
|
|
}
|
|
|
|
// Now delete it
|
|
deleteEvent := &RepoEvent{
|
|
Ops: []RepoOp{
|
|
{
|
|
Kind: EvtKindDeleteRecord,
|
|
Collection: "io.atcr.hold.crew",
|
|
Rkey: "testrkey",
|
|
},
|
|
},
|
|
}
|
|
handler(ctx, deleteEvent)
|
|
|
|
// Verify it was removed from index
|
|
count, _ = pds.RecordsIndex().Count("io.atcr.hold.crew")
|
|
if count != 0 {
|
|
t.Errorf("Expected 0 records after delete, got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_CreateRecordsIndexEventHandler_NilBroadcaster tests with nil broadcaster
|
|
func TestHoldPDS_CreateRecordsIndexEventHandler_NilBroadcaster(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Create handler with nil broadcaster (should not panic)
|
|
handler := pds.CreateRecordsIndexEventHandler(nil)
|
|
|
|
event := &RepoEvent{
|
|
Ops: []RepoOp{
|
|
{
|
|
Kind: EvtKindCreateRecord,
|
|
Collection: "io.atcr.hold.crew",
|
|
Rkey: "testrkey",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Should not panic
|
|
handler(ctx, event)
|
|
|
|
// Verify record was still indexed
|
|
count, _ := pds.RecordsIndex().Count("io.atcr.hold.crew")
|
|
if count != 1 {
|
|
t.Errorf("Expected 1 indexed record, got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_BackfillRecordsIndex tests backfilling the records index from MST
|
|
func TestHoldPDS_BackfillRecordsIndex(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Bootstrap to create some records in MST (captain + crew)
|
|
ownerDID := "did:plc:testowner"
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: ownerDID, Public: true})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Clear the index to simulate out-of-sync state
|
|
_, err = pds.RecordsIndex().db.Exec("DELETE FROM records")
|
|
if err != nil {
|
|
t.Fatalf("Failed to clear index: %v", err)
|
|
}
|
|
|
|
// Verify index is empty
|
|
count, _ := pds.RecordsIndex().TotalCount()
|
|
if count != 0 {
|
|
t.Fatalf("Expected empty index, got %d", count)
|
|
}
|
|
|
|
// Backfill
|
|
err = pds.BackfillRecordsIndex(ctx)
|
|
if err != nil {
|
|
t.Fatalf("BackfillRecordsIndex failed: %v", err)
|
|
}
|
|
|
|
// Verify records were backfilled
|
|
// Bootstrap creates: 1 captain + 1 crew + 1 profile = 3 records
|
|
count, _ = pds.RecordsIndex().TotalCount()
|
|
if count < 2 {
|
|
t.Errorf("Expected at least 2 records after backfill (captain + crew), got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_BackfillRecordsIndex_NilIndex tests backfill with nil index
|
|
func TestHoldPDS_BackfillRecordsIndex_NilIndex(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
// Use :memory: to get nil index
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", ":memory:", keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Backfill should be no-op and not error
|
|
err = pds.BackfillRecordsIndex(ctx)
|
|
if err != nil {
|
|
t.Errorf("BackfillRecordsIndex should not error with nil index, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHoldPDS_BackfillRecordsIndex_SkipsWhenSynced tests backfill skip when already synced
|
|
func TestHoldPDS_BackfillRecordsIndex_SkipsWhenSynced(t *testing.T) {
|
|
ctx := context.Background()
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "pds.db")
|
|
keyPath := filepath.Join(tmpDir, "signing-key")
|
|
|
|
pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", "https://atcr.io", dbPath, keyPath, false)
|
|
if err != nil {
|
|
t.Fatalf("NewHoldPDS failed: %v", err)
|
|
}
|
|
defer pds.Close()
|
|
|
|
// Bootstrap to create records
|
|
err = pds.Bootstrap(ctx, nil, BootstrapConfig{OwnerDID: "did:plc:testowner", Public: true})
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap failed: %v", err)
|
|
}
|
|
|
|
// Backfill once to sync
|
|
err = pds.BackfillRecordsIndex(ctx)
|
|
if err != nil {
|
|
t.Fatalf("First BackfillRecordsIndex failed: %v", err)
|
|
}
|
|
|
|
count1, _ := pds.RecordsIndex().TotalCount()
|
|
|
|
// Backfill again - should skip (counts match)
|
|
err = pds.BackfillRecordsIndex(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Second BackfillRecordsIndex failed: %v", err)
|
|
}
|
|
|
|
count2, _ := pds.RecordsIndex().TotalCount()
|
|
|
|
// Count should be unchanged
|
|
if count1 != count2 {
|
|
t.Errorf("Expected count to remain %d after second backfill, got %d", count1, count2)
|
|
}
|
|
}
|