Files

623 lines
18 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, 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)
crewAfter, err := pds.ListCrewMembers(ctx)
if err != nil {
t.Fatalf("ListCrewMembers failed after bootstrap: %v", err)
}
// Should have 2 crew members now: original + one added by bootstrap
// (Bootstrap doesn't check for duplicates currently)
if len(crewAfter) != 2 {
t.Logf("Note: Bootstrap added owner as crew even though they already existed")
t.Logf("Crew count after bootstrap: %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")
}
}
}