A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at codeberg-source 950 lines 28 kB view raw
1package pds 2 3import ( 4 "context" 5 "os" 6 "path/filepath" 7 "strings" 8 "testing" 9 10 "atcr.io/pkg/atproto" 11) 12 13// TestNewHoldPDS_NewRepo tests creating a new hold PDS with fresh database 14func TestNewHoldPDS_NewRepo(t *testing.T) { 15 ctx := context.Background() 16 tmpDir := t.TempDir() 17 18 // Paths for database and key 19 dbPath := filepath.Join(tmpDir, "pds.db") 20 keyPath := filepath.Join(tmpDir, "signing-key") 21 22 // Create new hold PDS 23 did := "did:web:hold.example.com" 24 publicURL := "https://hold.example.com" 25 26 pds, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, false) 27 if err != nil { 28 t.Fatalf("NewHoldPDS failed: %v", err) 29 } 30 defer pds.Close() 31 32 // Verify DID was set 33 if pds.DID() != did { 34 t.Errorf("Expected DID %s, got %s", did, pds.DID()) 35 } 36 37 // Verify signing key was created 38 if pds.SigningKey() == nil { 39 t.Error("Expected signing key to be created") 40 } 41 42 // Verify key file exists 43 if _, err := os.Stat(keyPath); os.IsNotExist(err) { 44 t.Error("Expected signing key file to be created") 45 } 46 47 // Verify database file exists (SQLite creates db.sqlite3 inside the directory) 48 dbFile := filepath.Join(dbPath, "db.sqlite3") 49 if _, err := os.Stat(dbFile); os.IsNotExist(err) { 50 t.Errorf("Expected database file to be created at %s", dbFile) 51 } 52} 53 54// TestNewHoldPDS_ExistingRepo tests opening an existing hold PDS database 55func TestNewHoldPDS_ExistingRepo(t *testing.T) { 56 ctx := context.Background() 57 tmpDir := t.TempDir() 58 59 dbPath := filepath.Join(tmpDir, "pds.db") 60 keyPath := filepath.Join(tmpDir, "signing-key") 61 did := "did:web:hold.example.com" 62 publicURL := "https://hold.example.com" 63 64 // Create first PDS instance and bootstrap it 65 pds1, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, false) 66 if err != nil { 67 t.Fatalf("First NewHoldPDS failed: %v", err) 68 } 69 70 // Bootstrap with a captain record 71 ownerDID := "did:plc:owner123" 72 if err := pds1.Bootstrap(ctx, nil, ownerDID, true, false, ""); err != nil { 73 t.Fatalf("Bootstrap failed: %v", err) 74 } 75 76 // Verify captain record exists 77 _, captain1, err := pds1.GetCaptainRecord(ctx) 78 if err != nil { 79 t.Fatalf("GetCaptainRecord failed after bootstrap: %v", err) 80 } 81 if captain1.Owner != ownerDID { 82 t.Errorf("Expected owner %s, got %s", ownerDID, captain1.Owner) 83 } 84 85 // Close first instance 86 pds1.Close() 87 88 // Re-open the same database 89 pds2, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, false) 90 if err != nil { 91 t.Fatalf("Second NewHoldPDS failed: %v", err) 92 } 93 defer pds2.Close() 94 95 // Verify captain record still exists 96 _, captain2, err := pds2.GetCaptainRecord(ctx) 97 if err != nil { 98 t.Fatalf("GetCaptainRecord failed after reopening: %v", err) 99 } 100 101 // Verify captain data persisted 102 if captain2.Owner != ownerDID { 103 t.Errorf("Expected owner %s after reopen, got %s", ownerDID, captain2.Owner) 104 } 105 if !captain2.Public { 106 t.Error("Expected captain.Public to be true") 107 } 108 if captain2.AllowAllCrew { 109 t.Error("Expected captain.AllowAllCrew to be false") 110 } 111} 112 113// TestBootstrap_NewRepo tests bootstrap on a new repository 114func TestBootstrap_NewRepo(t *testing.T) { 115 ctx := context.Background() 116 tmpDir := t.TempDir() 117 118 dbPath := filepath.Join(tmpDir, "pds.db") 119 keyPath := filepath.Join(tmpDir, "signing-key") 120 121 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 122 if err != nil { 123 t.Fatalf("NewHoldPDS failed: %v", err) 124 } 125 defer pds.Close() 126 127 // Bootstrap with owner 128 ownerDID := "did:plc:alice123" 129 publicAccess := true 130 allowAllCrew := false 131 132 err = pds.Bootstrap(ctx, nil, ownerDID, publicAccess, allowAllCrew, "") 133 if err != nil { 134 t.Fatalf("Bootstrap failed: %v", err) 135 } 136 137 // Verify captain record was created 138 _, captain, err := pds.GetCaptainRecord(ctx) 139 if err != nil { 140 t.Fatalf("GetCaptainRecord failed: %v", err) 141 } 142 143 // Verify captain fields 144 if captain.Owner != ownerDID { 145 t.Errorf("Expected owner %s, got %s", ownerDID, captain.Owner) 146 } 147 if captain.Public != publicAccess { 148 t.Errorf("Expected public=%v, got %v", publicAccess, captain.Public) 149 } 150 if captain.AllowAllCrew != allowAllCrew { 151 t.Errorf("Expected allowAllCrew=%v, got %v", allowAllCrew, captain.AllowAllCrew) 152 } 153 if captain.Type != atproto.CaptainCollection { 154 t.Errorf("Expected type %s, got %s", atproto.CaptainCollection, captain.Type) 155 } 156 if captain.DeployedAt == "" { 157 t.Error("Expected deployedAt to be set") 158 } 159 160 // Verify owner was added as crew member 161 crewMembers, err := pds.ListCrewMembers(ctx) 162 if err != nil { 163 t.Fatalf("ListCrewMembers failed: %v", err) 164 } 165 166 if len(crewMembers) != 1 { 167 t.Fatalf("Expected 1 crew member, got %d", len(crewMembers)) 168 } 169 170 crew := crewMembers[0] 171 if crew.Record.Member != ownerDID { 172 t.Errorf("Expected crew member %s, got %s", ownerDID, crew.Record.Member) 173 } 174 if crew.Record.Role != "admin" { 175 t.Errorf("Expected role admin, got %s", crew.Record.Role) 176 } 177 178 // Verify permissions 179 expectedPerms := []string{"blob:read", "blob:write", "crew:admin"} 180 if len(crew.Record.Permissions) != len(expectedPerms) { 181 t.Fatalf("Expected %d permissions, got %d", len(expectedPerms), len(crew.Record.Permissions)) 182 } 183 for i, perm := range expectedPerms { 184 if crew.Record.Permissions[i] != perm { 185 t.Errorf("Expected permission[%d]=%s, got %s", i, perm, crew.Record.Permissions[i]) 186 } 187 } 188} 189 190// TestBootstrap_Idempotent tests that bootstrap is idempotent 191func TestBootstrap_Idempotent(t *testing.T) { 192 ctx := context.Background() 193 tmpDir := t.TempDir() 194 195 dbPath := filepath.Join(tmpDir, "pds.db") 196 keyPath := filepath.Join(tmpDir, "signing-key") 197 198 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 199 if err != nil { 200 t.Fatalf("NewHoldPDS failed: %v", err) 201 } 202 defer pds.Close() 203 204 ownerDID := "did:plc:alice123" 205 206 // First bootstrap 207 err = pds.Bootstrap(ctx, nil, ownerDID, true, false, "") 208 if err != nil { 209 t.Fatalf("First bootstrap failed: %v", err) 210 } 211 212 // Get captain CID after first bootstrap 213 cid1, captain1, err := pds.GetCaptainRecord(ctx) 214 if err != nil { 215 t.Fatalf("GetCaptainRecord failed: %v", err) 216 } 217 218 // Get crew count after first bootstrap 219 crew1, err := pds.ListCrewMembers(ctx) 220 if err != nil { 221 t.Fatalf("ListCrewMembers failed: %v", err) 222 } 223 crewCount1 := len(crew1) 224 225 // Second bootstrap (should be idempotent - skip creation) 226 err = pds.Bootstrap(ctx, nil, ownerDID, true, false, "") 227 if err != nil { 228 t.Fatalf("Second bootstrap failed: %v", err) 229 } 230 231 // Verify captain record unchanged 232 cid2, captain2, err := pds.GetCaptainRecord(ctx) 233 if err != nil { 234 t.Fatalf("GetCaptainRecord failed after second bootstrap: %v", err) 235 } 236 237 if !cid1.Equals(cid2) { 238 t.Error("Expected captain CID to remain unchanged after second bootstrap") 239 } 240 if captain1.Owner != captain2.Owner { 241 t.Error("Expected captain owner to remain unchanged") 242 } 243 244 // Verify crew count unchanged (owner not added twice) 245 crew2, err := pds.ListCrewMembers(ctx) 246 if err != nil { 247 t.Fatalf("ListCrewMembers failed after second bootstrap: %v", err) 248 } 249 crewCount2 := len(crew2) 250 251 if crewCount1 != crewCount2 { 252 t.Errorf("Expected crew count to remain %d, got %d (owner may have been added twice)", crewCount1, crewCount2) 253 } 254} 255 256// TestBootstrap_EmptyOwner tests that bootstrap with empty owner is a no-op 257func TestBootstrap_EmptyOwner(t *testing.T) { 258 ctx := context.Background() 259 tmpDir := t.TempDir() 260 261 dbPath := filepath.Join(tmpDir, "pds.db") 262 keyPath := filepath.Join(tmpDir, "signing-key") 263 264 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 265 if err != nil { 266 t.Fatalf("NewHoldPDS failed: %v", err) 267 } 268 defer pds.Close() 269 270 // Bootstrap with empty owner DID (should be no-op) 271 err = pds.Bootstrap(ctx, nil, "", true, false, "") 272 if err != nil { 273 t.Fatalf("Bootstrap with empty owner should not error: %v", err) 274 } 275 276 // Verify captain record was NOT created 277 _, _, err = pds.GetCaptainRecord(ctx) 278 if err == nil { 279 t.Error("Expected GetCaptainRecord to fail (no captain record), but it succeeded") 280 } 281 // Verify it's a "not found" type error 282 if err != nil && !strings.Contains(err.Error(), "not found") && !strings.Contains(err.Error(), "failed to get captain record") { 283 t.Errorf("Expected 'not found' error, got: %v", err) 284 } 285} 286 287// TestLexiconTypeRegistration tests that captain and crew types are registered 288func TestLexiconTypeRegistration(t *testing.T) { 289 // The init() function in server.go registers types 290 // We can verify this by creating a PDS and doing a round-trip write/read 291 ctx := context.Background() 292 tmpDir := t.TempDir() 293 294 dbPath := filepath.Join(tmpDir, "pds.db") 295 keyPath := filepath.Join(tmpDir, "signing-key") 296 297 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 298 if err != nil { 299 t.Fatalf("NewHoldPDS failed: %v", err) 300 } 301 defer pds.Close() 302 303 // Bootstrap to create captain record 304 ownerDID := "did:plc:alice123" 305 if err := pds.Bootstrap(ctx, nil, ownerDID, true, false, ""); err != nil { 306 t.Fatalf("Bootstrap failed: %v", err) 307 } 308 309 // GetCaptainRecord uses type assertion to *atproto.CaptainRecord 310 // If the type wasn't registered, this would fail with type assertion error 311 _, captain, err := pds.GetCaptainRecord(ctx) 312 if err != nil { 313 t.Fatalf("GetCaptainRecord failed: %v", err) 314 } 315 316 // Verify we got the correct concrete type 317 if captain == nil { 318 t.Fatal("Expected non-nil captain record") 319 } 320 if captain.Type != atproto.CaptainCollection { 321 t.Errorf("Expected captain type %s, got %s", atproto.CaptainCollection, captain.Type) 322 } 323 324 // Do the same for crew record 325 crewMembers, err := pds.ListCrewMembers(ctx) 326 if err != nil { 327 t.Fatalf("ListCrewMembers failed: %v", err) 328 } 329 if len(crewMembers) == 0 { 330 t.Fatal("Expected at least one crew member") 331 } 332 333 crew := crewMembers[0].Record 334 if crew.Type != atproto.CrewCollection { 335 t.Errorf("Expected crew type %s, got %s", atproto.CrewCollection, crew.Type) 336 } 337} 338 339// TestBootstrap_DidWebOwner tests bootstrap with did:web owner 340func TestBootstrap_DidWebOwner(t *testing.T) { 341 ctx := context.Background() 342 tmpDir := t.TempDir() 343 344 dbPath := filepath.Join(tmpDir, "pds.db") 345 keyPath := filepath.Join(tmpDir, "signing-key") 346 347 pds, err := NewHoldPDS(ctx, "did:web:hold01.atcr.io", "https://hold01.atcr.io", dbPath, keyPath, false) 348 if err != nil { 349 t.Fatalf("NewHoldPDS failed: %v", err) 350 } 351 defer pds.Close() 352 353 // Bootstrap with did:web owner (not did:plc) 354 ownerDID := "did:web:alice.example.com" 355 publicAccess := true 356 allowAllCrew := false 357 358 err = pds.Bootstrap(ctx, nil, ownerDID, publicAccess, allowAllCrew, "") 359 if err != nil { 360 t.Fatalf("Bootstrap failed with did:web owner: %v", err) 361 } 362 363 // Verify captain record was created with did:web owner 364 _, captain, err := pds.GetCaptainRecord(ctx) 365 if err != nil { 366 t.Fatalf("GetCaptainRecord failed: %v", err) 367 } 368 369 // Verify captain fields 370 if captain.Owner != ownerDID { 371 t.Errorf("Expected owner %s, got %s", ownerDID, captain.Owner) 372 } 373 if captain.Public != publicAccess { 374 t.Errorf("Expected public=%v, got %v", publicAccess, captain.Public) 375 } 376 if captain.AllowAllCrew != allowAllCrew { 377 t.Errorf("Expected allowAllCrew=%v, got %v", allowAllCrew, captain.AllowAllCrew) 378 } 379 380 // Verify owner was added as crew member 381 crewMembers, err := pds.ListCrewMembers(ctx) 382 if err != nil { 383 t.Fatalf("ListCrewMembers failed: %v", err) 384 } 385 386 if len(crewMembers) != 1 { 387 t.Fatalf("Expected 1 crew member, got %d", len(crewMembers)) 388 } 389 390 crew := crewMembers[0] 391 if crew.Record.Member != ownerDID { 392 t.Errorf("Expected crew member %s, got %s", ownerDID, crew.Record.Member) 393 } 394 if crew.Record.Role != "admin" { 395 t.Errorf("Expected role admin, got %s", crew.Record.Role) 396 } 397} 398 399// TestBootstrap_MixedDIDs tests bootstrap with mixed DID types 400func TestBootstrap_MixedDIDs(t *testing.T) { 401 ctx := context.Background() 402 tmpDir := t.TempDir() 403 404 dbPath := filepath.Join(tmpDir, "pds.db") 405 keyPath := filepath.Join(tmpDir, "signing-key") 406 407 // Create hold with did:web 408 holdDID := "did:web:hold.example.com" 409 pds, err := NewHoldPDS(ctx, holdDID, "https://hold.example.com", dbPath, keyPath, false) 410 if err != nil { 411 t.Fatalf("NewHoldPDS failed: %v", err) 412 } 413 defer pds.Close() 414 415 // Bootstrap with did:plc owner 416 plcOwner := "did:plc:alice123" 417 err = pds.Bootstrap(ctx, nil, plcOwner, true, false, "") 418 if err != nil { 419 t.Fatalf("Bootstrap failed: %v", err) 420 } 421 422 // Add did:web crew member 423 webMember := "did:web:bob.example.com" 424 _, err = pds.AddCrewMember(ctx, webMember, "writer", []string{"blob:read", "blob:write"}) 425 if err != nil { 426 t.Fatalf("AddCrewMember failed with did:web: %v", err) 427 } 428 429 // Verify captain 430 _, captain, err := pds.GetCaptainRecord(ctx) 431 if err != nil { 432 t.Fatalf("GetCaptainRecord failed: %v", err) 433 } 434 if captain.Owner != plcOwner { 435 t.Errorf("Expected captain owner %s, got %s", plcOwner, captain.Owner) 436 } 437 438 // Verify crew members (should have both did:plc and did:web) 439 crewMembers, err := pds.ListCrewMembers(ctx) 440 if err != nil { 441 t.Fatalf("ListCrewMembers failed: %v", err) 442 } 443 444 if len(crewMembers) != 2 { 445 t.Fatalf("Expected 2 crew members, got %d", len(crewMembers)) 446 } 447 448 // Verify both DIDs are present 449 foundPLC := false 450 foundWeb := false 451 for _, cm := range crewMembers { 452 if cm.Record.Member == plcOwner { 453 foundPLC = true 454 } 455 if cm.Record.Member == webMember { 456 foundWeb = true 457 } 458 } 459 460 if !foundPLC { 461 t.Errorf("Expected to find did:plc member %s", plcOwner) 462 } 463 if !foundWeb { 464 t.Errorf("Expected to find did:web member %s", webMember) 465 } 466} 467 468// TestBootstrap_CrewWithoutCaptain tests bootstrap when crew exists but captain doesn't 469// This edge case could happen if repo state is corrupted or partially initialized 470func TestBootstrap_CrewWithoutCaptain(t *testing.T) { 471 ctx := context.Background() 472 tmpDir := t.TempDir() 473 474 dbPath := filepath.Join(tmpDir, "pds.db") 475 keyPath := filepath.Join(tmpDir, "signing-key") 476 477 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 478 if err != nil { 479 t.Fatalf("NewHoldPDS failed: %v", err) 480 } 481 defer pds.Close() 482 483 // Initialize repo manually 484 err = pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "") 485 if err != nil { 486 t.Fatalf("InitNewActor failed: %v", err) 487 } 488 489 // Create crew member WITHOUT captain (unusual state) 490 ownerDID := "did:plc:alice123" 491 _, err = pds.AddCrewMember(ctx, ownerDID, "admin", []string{"blob:read", "blob:write", "crew:admin"}) 492 if err != nil { 493 t.Fatalf("AddCrewMember failed: %v", err) 494 } 495 496 // Verify crew exists 497 crewBefore, err := pds.ListCrewMembers(ctx) 498 if err != nil { 499 t.Fatalf("ListCrewMembers failed: %v", err) 500 } 501 if len(crewBefore) != 1 { 502 t.Fatalf("Expected 1 crew member before bootstrap, got %d", len(crewBefore)) 503 } 504 505 // Verify captain doesn't exist 506 _, _, err = pds.GetCaptainRecord(ctx) 507 if err == nil { 508 t.Fatal("Expected captain record to not exist before bootstrap") 509 } 510 511 // Bootstrap should create captain record 512 err = pds.Bootstrap(ctx, nil, ownerDID, true, false, "") 513 if err != nil { 514 t.Fatalf("Bootstrap failed: %v", err) 515 } 516 517 // Verify captain was created 518 _, captain, err := pds.GetCaptainRecord(ctx) 519 if err != nil { 520 t.Fatalf("GetCaptainRecord failed after bootstrap: %v", err) 521 } 522 if captain.Owner != ownerDID { 523 t.Errorf("Expected captain owner %s, got %s", ownerDID, captain.Owner) 524 } 525 526 // Verify crew wasn't duplicated (Bootstrap adds owner as crew, but they already exist) 527 crewAfter, err := pds.ListCrewMembers(ctx) 528 if err != nil { 529 t.Fatalf("ListCrewMembers failed after bootstrap: %v", err) 530 } 531 532 // Should have 2 crew members now: original + one added by bootstrap 533 // (Bootstrap doesn't check for duplicates currently) 534 if len(crewAfter) != 2 { 535 t.Logf("Note: Bootstrap added owner as crew even though they already existed") 536 t.Logf("Crew count after bootstrap: %d", len(crewAfter)) 537 } 538} 539 540// TestBootstrap_CaptainWithoutCrew tests bootstrap when captain exists but owner crew doesn't 541// This verifies that bootstrap properly adds the owner as crew if missing 542func TestBootstrap_CaptainWithoutCrew(t *testing.T) { 543 ctx := context.Background() 544 tmpDir := t.TempDir() 545 546 dbPath := filepath.Join(tmpDir, "pds.db") 547 keyPath := filepath.Join(tmpDir, "signing-key") 548 549 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 550 if err != nil { 551 t.Fatalf("NewHoldPDS failed: %v", err) 552 } 553 defer pds.Close() 554 555 // Initialize repo manually 556 err = pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "") 557 if err != nil { 558 t.Fatalf("InitNewActor failed: %v", err) 559 } 560 561 // Create captain record WITHOUT crew (unusual state) 562 ownerDID := "did:plc:alice123" 563 _, err = pds.CreateCaptainRecord(ctx, ownerDID, true, false, false) 564 if err != nil { 565 t.Fatalf("CreateCaptainRecord failed: %v", err) 566 } 567 568 // Verify captain exists 569 _, captain, err := pds.GetCaptainRecord(ctx) 570 if err != nil { 571 t.Fatalf("GetCaptainRecord failed: %v", err) 572 } 573 if captain.Owner != ownerDID { 574 t.Errorf("Expected captain owner %s, got %s", ownerDID, captain.Owner) 575 } 576 577 // Verify crew is empty 578 crewBefore, err := pds.ListCrewMembers(ctx) 579 if err != nil { 580 t.Fatalf("ListCrewMembers failed: %v", err) 581 } 582 if len(crewBefore) != 0 { 583 t.Fatalf("Expected 0 crew members before bootstrap, got %d", len(crewBefore)) 584 } 585 586 // Bootstrap should be idempotent but notice missing crew 587 // Currently Bootstrap skips if captain exists, so crew won't be added 588 err = pds.Bootstrap(ctx, nil, ownerDID, true, false, "") 589 if err != nil { 590 t.Fatalf("Bootstrap failed: %v", err) 591 } 592 593 // Verify crew after bootstrap 594 crewAfter, err := pds.ListCrewMembers(ctx) 595 if err != nil { 596 t.Fatalf("ListCrewMembers failed after bootstrap: %v", err) 597 } 598 599 // Bootstrap currently skips everything if captain exists 600 // This means crew won't be added in this case 601 if len(crewAfter) == 0 { 602 t.Logf("Note: Bootstrap skipped adding owner as crew because captain already exists") 603 t.Logf("This is current behavior - Bootstrap is fully idempotent and skips if captain exists") 604 } else { 605 // If we change Bootstrap to be smarter, it might add crew 606 t.Logf("Bootstrap added %d crew members", len(crewAfter)) 607 608 // Verify owner was added 609 foundOwner := false 610 for _, cm := range crewAfter { 611 if cm.Record.Member == ownerDID { 612 foundOwner = true 613 if cm.Record.Role != "admin" { 614 t.Errorf("Expected owner role admin, got %s", cm.Record.Role) 615 } 616 } 617 } 618 if !foundOwner { 619 t.Error("Expected owner to be added as crew member") 620 } 621 } 622} 623 624// Tests for RecordsIndex feature 625 626// TestHoldPDS_RecordsIndex_Nil tests that RecordsIndex is nil for :memory: database 627func TestHoldPDS_RecordsIndex_Nil(t *testing.T) { 628 ctx := context.Background() 629 tmpDir := t.TempDir() 630 keyPath := filepath.Join(tmpDir, "signing-key") 631 632 // Create with :memory: database 633 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", ":memory:", keyPath, false) 634 if err != nil { 635 t.Fatalf("NewHoldPDS failed: %v", err) 636 } 637 defer pds.Close() 638 639 // RecordsIndex should be nil for :memory: 640 if pds.RecordsIndex() != nil { 641 t.Error("Expected RecordsIndex() to be nil for :memory: database") 642 } 643} 644 645// TestHoldPDS_RecordsIndex_NonNil tests that RecordsIndex is created for file database 646func TestHoldPDS_RecordsIndex_NonNil(t *testing.T) { 647 ctx := context.Background() 648 tmpDir := t.TempDir() 649 dbPath := filepath.Join(tmpDir, "pds.db") 650 keyPath := filepath.Join(tmpDir, "signing-key") 651 652 // Create with file database 653 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 654 if err != nil { 655 t.Fatalf("NewHoldPDS failed: %v", err) 656 } 657 defer pds.Close() 658 659 // RecordsIndex should be non-nil for file database 660 if pds.RecordsIndex() == nil { 661 t.Error("Expected RecordsIndex() to be non-nil for file database") 662 } 663} 664 665// TestHoldPDS_Carstore tests the Carstore getter 666func TestHoldPDS_Carstore(t *testing.T) { 667 ctx := context.Background() 668 tmpDir := t.TempDir() 669 keyPath := filepath.Join(tmpDir, "signing-key") 670 671 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", ":memory:", keyPath, false) 672 if err != nil { 673 t.Fatalf("NewHoldPDS failed: %v", err) 674 } 675 defer pds.Close() 676 677 if pds.Carstore() == nil { 678 t.Error("Expected Carstore() to be non-nil") 679 } 680} 681 682// TestHoldPDS_UID tests the UID getter 683func TestHoldPDS_UID(t *testing.T) { 684 ctx := context.Background() 685 tmpDir := t.TempDir() 686 keyPath := filepath.Join(tmpDir, "signing-key") 687 688 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", ":memory:", keyPath, false) 689 if err != nil { 690 t.Fatalf("NewHoldPDS failed: %v", err) 691 } 692 defer pds.Close() 693 694 // UID should be 1 for single-user PDS 695 if pds.UID() != 1 { 696 t.Errorf("Expected UID() to be 1, got %d", pds.UID()) 697 } 698} 699 700// TestHoldPDS_CreateRecordsIndexEventHandler tests event handler wrapper 701func TestHoldPDS_CreateRecordsIndexEventHandler(t *testing.T) { 702 ctx := context.Background() 703 tmpDir := t.TempDir() 704 dbPath := filepath.Join(tmpDir, "pds.db") 705 keyPath := filepath.Join(tmpDir, "signing-key") 706 707 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 708 if err != nil { 709 t.Fatalf("NewHoldPDS failed: %v", err) 710 } 711 defer pds.Close() 712 713 // Track if broadcaster was called 714 broadcasterCalled := false 715 broadcasterHandler := func(ctx context.Context, event *RepoEvent) { 716 broadcasterCalled = true 717 } 718 719 // Create handler 720 handler := pds.CreateRecordsIndexEventHandler(broadcasterHandler) 721 if handler == nil { 722 t.Fatal("Expected handler to be non-nil") 723 } 724 725 // Create a test event with create operation 726 event := &RepoEvent{ 727 Ops: []RepoOp{ 728 { 729 Kind: EvtKindCreateRecord, 730 Collection: "io.atcr.hold.crew", 731 Rkey: "testrkey", 732 RecCid: nil, // Will be nil string 733 }, 734 }, 735 } 736 737 // Call handler 738 handler(ctx, event) 739 740 // Verify broadcaster was called 741 if !broadcasterCalled { 742 t.Error("Expected broadcaster handler to be called") 743 } 744 745 // Verify record was indexed 746 if pds.RecordsIndex() != nil { 747 count, err := pds.RecordsIndex().Count("io.atcr.hold.crew") 748 if err != nil { 749 t.Fatalf("Count() error = %v", err) 750 } 751 if count != 1 { 752 t.Errorf("Expected 1 indexed record, got %d", count) 753 } 754 } 755} 756 757// TestHoldPDS_CreateRecordsIndexEventHandler_Delete tests delete operation 758func TestHoldPDS_CreateRecordsIndexEventHandler_Delete(t *testing.T) { 759 ctx := context.Background() 760 tmpDir := t.TempDir() 761 dbPath := filepath.Join(tmpDir, "pds.db") 762 keyPath := filepath.Join(tmpDir, "signing-key") 763 764 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 765 if err != nil { 766 t.Fatalf("NewHoldPDS failed: %v", err) 767 } 768 defer pds.Close() 769 770 handler := pds.CreateRecordsIndexEventHandler(nil) 771 772 // First, create a record 773 createEvent := &RepoEvent{ 774 Ops: []RepoOp{ 775 { 776 Kind: EvtKindCreateRecord, 777 Collection: "io.atcr.hold.crew", 778 Rkey: "testrkey", 779 }, 780 }, 781 } 782 handler(ctx, createEvent) 783 784 // Verify it was indexed 785 count, _ := pds.RecordsIndex().Count("io.atcr.hold.crew") 786 if count != 1 { 787 t.Fatalf("Expected 1 record after create, got %d", count) 788 } 789 790 // Now delete it 791 deleteEvent := &RepoEvent{ 792 Ops: []RepoOp{ 793 { 794 Kind: EvtKindDeleteRecord, 795 Collection: "io.atcr.hold.crew", 796 Rkey: "testrkey", 797 }, 798 }, 799 } 800 handler(ctx, deleteEvent) 801 802 // Verify it was removed from index 803 count, _ = pds.RecordsIndex().Count("io.atcr.hold.crew") 804 if count != 0 { 805 t.Errorf("Expected 0 records after delete, got %d", count) 806 } 807} 808 809// TestHoldPDS_CreateRecordsIndexEventHandler_NilBroadcaster tests with nil broadcaster 810func TestHoldPDS_CreateRecordsIndexEventHandler_NilBroadcaster(t *testing.T) { 811 ctx := context.Background() 812 tmpDir := t.TempDir() 813 dbPath := filepath.Join(tmpDir, "pds.db") 814 keyPath := filepath.Join(tmpDir, "signing-key") 815 816 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 817 if err != nil { 818 t.Fatalf("NewHoldPDS failed: %v", err) 819 } 820 defer pds.Close() 821 822 // Create handler with nil broadcaster (should not panic) 823 handler := pds.CreateRecordsIndexEventHandler(nil) 824 825 event := &RepoEvent{ 826 Ops: []RepoOp{ 827 { 828 Kind: EvtKindCreateRecord, 829 Collection: "io.atcr.hold.crew", 830 Rkey: "testrkey", 831 }, 832 }, 833 } 834 835 // Should not panic 836 handler(ctx, event) 837 838 // Verify record was still indexed 839 count, _ := pds.RecordsIndex().Count("io.atcr.hold.crew") 840 if count != 1 { 841 t.Errorf("Expected 1 indexed record, got %d", count) 842 } 843} 844 845// TestHoldPDS_BackfillRecordsIndex tests backfilling the records index from MST 846func TestHoldPDS_BackfillRecordsIndex(t *testing.T) { 847 ctx := context.Background() 848 tmpDir := t.TempDir() 849 dbPath := filepath.Join(tmpDir, "pds.db") 850 keyPath := filepath.Join(tmpDir, "signing-key") 851 852 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 853 if err != nil { 854 t.Fatalf("NewHoldPDS failed: %v", err) 855 } 856 defer pds.Close() 857 858 // Bootstrap to create some records in MST (captain + crew) 859 ownerDID := "did:plc:testowner" 860 err = pds.Bootstrap(ctx, nil, ownerDID, true, false, "") 861 if err != nil { 862 t.Fatalf("Bootstrap failed: %v", err) 863 } 864 865 // Clear the index to simulate out-of-sync state 866 _, err = pds.RecordsIndex().db.Exec("DELETE FROM records") 867 if err != nil { 868 t.Fatalf("Failed to clear index: %v", err) 869 } 870 871 // Verify index is empty 872 count, _ := pds.RecordsIndex().TotalCount() 873 if count != 0 { 874 t.Fatalf("Expected empty index, got %d", count) 875 } 876 877 // Backfill 878 err = pds.BackfillRecordsIndex(ctx) 879 if err != nil { 880 t.Fatalf("BackfillRecordsIndex failed: %v", err) 881 } 882 883 // Verify records were backfilled 884 // Bootstrap creates: 1 captain + 1 crew + 1 profile = 3 records 885 count, _ = pds.RecordsIndex().TotalCount() 886 if count < 2 { 887 t.Errorf("Expected at least 2 records after backfill (captain + crew), got %d", count) 888 } 889} 890 891// TestHoldPDS_BackfillRecordsIndex_NilIndex tests backfill with nil index 892func TestHoldPDS_BackfillRecordsIndex_NilIndex(t *testing.T) { 893 ctx := context.Background() 894 tmpDir := t.TempDir() 895 keyPath := filepath.Join(tmpDir, "signing-key") 896 897 // Use :memory: to get nil index 898 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", ":memory:", keyPath, false) 899 if err != nil { 900 t.Fatalf("NewHoldPDS failed: %v", err) 901 } 902 defer pds.Close() 903 904 // Backfill should be no-op and not error 905 err = pds.BackfillRecordsIndex(ctx) 906 if err != nil { 907 t.Errorf("BackfillRecordsIndex should not error with nil index, got: %v", err) 908 } 909} 910 911// TestHoldPDS_BackfillRecordsIndex_SkipsWhenSynced tests backfill skip when already synced 912func TestHoldPDS_BackfillRecordsIndex_SkipsWhenSynced(t *testing.T) { 913 ctx := context.Background() 914 tmpDir := t.TempDir() 915 dbPath := filepath.Join(tmpDir, "pds.db") 916 keyPath := filepath.Join(tmpDir, "signing-key") 917 918 pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 919 if err != nil { 920 t.Fatalf("NewHoldPDS failed: %v", err) 921 } 922 defer pds.Close() 923 924 // Bootstrap to create records 925 err = pds.Bootstrap(ctx, nil, "did:plc:testowner", true, false, "") 926 if err != nil { 927 t.Fatalf("Bootstrap failed: %v", err) 928 } 929 930 // Backfill once to sync 931 err = pds.BackfillRecordsIndex(ctx) 932 if err != nil { 933 t.Fatalf("First BackfillRecordsIndex failed: %v", err) 934 } 935 936 count1, _ := pds.RecordsIndex().TotalCount() 937 938 // Backfill again - should skip (counts match) 939 err = pds.BackfillRecordsIndex(ctx) 940 if err != nil { 941 t.Fatalf("Second BackfillRecordsIndex failed: %v", err) 942 } 943 944 count2, _ := pds.RecordsIndex().TotalCount() 945 946 // Count should be unchanged 947 if count1 != count2 { 948 t.Errorf("Expected count to remain %d after second backfill, got %d", count1, count2) 949 } 950}