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, 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, 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, ownerDID, true, false, "", ""); 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, 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", 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, ownerDID, publicAccess, 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", 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, ownerDID, true, false, "", "") 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, ownerDID, true, false, "", "") 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", 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, "", true, false, "", "") 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", 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, ownerDID, true, false, "", ""); 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", 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, ownerDID, publicAccess, 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", 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, plcOwner, true, false, "", "") 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", 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, ownerDID, true, false, "", "") 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", 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, ownerDID, true, false, "", "") 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", ":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", 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", ":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", ":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", 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", 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", 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", 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, ownerDID, true, false, "", "") 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", ":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", dbPath, keyPath, false) if err != nil { t.Fatalf("NewHoldPDS failed: %v", err) } defer pds.Close() // Bootstrap to create records err = pds.Bootstrap(ctx, nil, "did:plc:testowner", true, false, "", "") 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) } }