Vibe-guided bskyoauth and custom repo example code in Golang 馃 probably not safe to use in prod
at main 24 kB view raw
1package bskyoauth 2 3import ( 4 "context" 5 "crypto/ecdsa" 6 "crypto/elliptic" 7 "crypto/rand" 8 "encoding/json" 9 "net/http" 10 "net/http/httptest" 11 "testing" 12) 13 14// TestNewClient verifies basic client creation with defaults. 15func TestNewClient(t *testing.T) { 16 baseURL := "http://localhost:8181" 17 client := NewClient(baseURL) 18 19 if client == nil { 20 t.Fatal("NewClient returned nil") 21 } 22 23 if client.BaseURL != baseURL { 24 t.Errorf("BaseURL: expected %s, got %s", baseURL, client.BaseURL) 25 } 26 27 expectedClientID := baseURL + "/oauth-client-metadata.json" 28 if client.ClientID != expectedClientID { 29 t.Errorf("ClientID: expected %s, got %s", expectedClientID, client.ClientID) 30 } 31 32 expectedRedirectURI := baseURL + "/callback" 33 if client.RedirectURI != expectedRedirectURI { 34 t.Errorf("RedirectURI: expected %s, got %s", expectedRedirectURI, client.RedirectURI) 35 } 36 37 if client.ClientName != "Bluesky OAuth Client" { 38 t.Errorf("ClientName: expected 'Bluesky OAuth Client', got %s", client.ClientName) 39 } 40 41 if len(client.Scopes) != 2 || client.Scopes[0] != "atproto" || client.Scopes[1] != "transition:generic" { 42 t.Errorf("Scopes: expected ['atproto', 'transition:generic'], got %v", client.Scopes) 43 } 44 45 if client.SessionStore == nil { 46 t.Error("SessionStore should be initialized with default store") 47 } 48} 49 50// TestNewClientWithTrailingSlash verifies trailing slash is removed from BaseURL. 51func TestNewClientWithTrailingSlash(t *testing.T) { 52 baseURL := "http://localhost:8181/" 53 client := NewClient(baseURL) 54 55 expectedBaseURL := "http://localhost:8181" 56 if client.BaseURL != expectedBaseURL { 57 t.Errorf("BaseURL: expected %s, got %s", expectedBaseURL, client.BaseURL) 58 } 59 60 expectedClientID := "http://localhost:8181/oauth-client-metadata.json" 61 if client.ClientID != expectedClientID { 62 t.Errorf("ClientID: expected %s, got %s", expectedClientID, client.ClientID) 63 } 64} 65 66// TestNewClientWithOptions verifies custom client configuration. 67func TestNewClientWithOptions(t *testing.T) { 68 customStore := NewMemorySessionStore() 69 opts := ClientOptions{ 70 BaseURL: "https://example.com", 71 ClientName: "Custom OAuth Client", 72 Scopes: []string{"custom:scope", "another:scope"}, 73 SessionStore: customStore, 74 } 75 76 client := NewClientWithOptions(opts) 77 78 if client.BaseURL != "https://example.com" { 79 t.Errorf("BaseURL: expected https://example.com, got %s", client.BaseURL) 80 } 81 82 if client.ClientName != "Custom OAuth Client" { 83 t.Errorf("ClientName: expected 'Custom OAuth Client', got %s", client.ClientName) 84 } 85 86 if len(client.Scopes) != 2 || client.Scopes[0] != "custom:scope" { 87 t.Errorf("Scopes: expected custom scopes, got %v", client.Scopes) 88 } 89 90 if client.SessionStore != customStore { 91 t.Error("SessionStore: expected custom store to be used") 92 } 93} 94 95// TestNewClientWithOptionsDefaults verifies defaults are applied when options are empty. 96func TestNewClientWithOptionsDefaults(t *testing.T) { 97 opts := ClientOptions{ 98 BaseURL: "http://localhost:3000", 99 // ClientName, Scopes, SessionStore not provided 100 } 101 102 client := NewClientWithOptions(opts) 103 104 if client.ClientName != "Bluesky OAuth Client" { 105 t.Errorf("ClientName default: expected 'Bluesky OAuth Client', got %s", client.ClientName) 106 } 107 108 if len(client.Scopes) != 2 || client.Scopes[0] != "atproto" { 109 t.Errorf("Scopes default: expected ['atproto', 'transition:generic'], got %v", client.Scopes) 110 } 111 112 if client.SessionStore == nil { 113 t.Error("SessionStore default: should be initialized") 114 } 115} 116 117// TestGetClientMetadata verifies client metadata structure. 118func TestGetClientMetadata(t *testing.T) { 119 client := NewClient("https://oauth.example.com") 120 metadata := client.GetClientMetadata() 121 122 // Verify required fields 123 if metadata["client_id"] != "https://oauth.example.com/oauth-client-metadata.json" { 124 t.Errorf("client_id: expected https://oauth.example.com/oauth-client-metadata.json, got %v", metadata["client_id"]) 125 } 126 127 if metadata["client_name"] != "Bluesky OAuth Client" { 128 t.Errorf("client_name: expected 'Bluesky OAuth Client', got %v", metadata["client_name"]) 129 } 130 131 redirectURIs, ok := metadata["redirect_uris"].([]string) 132 if !ok || len(redirectURIs) != 1 || redirectURIs[0] != "https://oauth.example.com/callback" { 133 t.Errorf("redirect_uris: expected ['https://oauth.example.com/callback'], got %v", metadata["redirect_uris"]) 134 } 135 136 if metadata["scope"] != "atproto transition:generic" { 137 t.Errorf("scope: expected 'atproto transition:generic', got %v", metadata["scope"]) 138 } 139 140 grantTypes, ok := metadata["grant_types"].([]string) 141 if !ok || len(grantTypes) != 2 { 142 t.Errorf("grant_types: expected 2 types, got %v", metadata["grant_types"]) 143 } 144 145 if metadata["token_endpoint_auth_method"] != "none" { 146 t.Errorf("token_endpoint_auth_method: expected 'none', got %v", metadata["token_endpoint_auth_method"]) 147 } 148 149 if metadata["application_type"] != "web" { 150 t.Errorf("application_type: expected 'web', got %v", metadata["application_type"]) 151 } 152 153 if metadata["dpop_bound_access_tokens"] != true { 154 t.Errorf("dpop_bound_access_tokens: expected true, got %v", metadata["dpop_bound_access_tokens"]) 155 } 156} 157 158// TestClientMetadataHandler verifies the HTTP handler for client metadata. 159func TestClientMetadataHandler(t *testing.T) { 160 client := NewClient("https://oauth.example.com") 161 handler := client.ClientMetadataHandler() 162 163 req := httptest.NewRequest("GET", "/client-metadata.json", nil) 164 w := httptest.NewRecorder() 165 166 handler(w, req) 167 168 if w.Code != http.StatusOK { 169 t.Errorf("Status code: expected 200, got %d", w.Code) 170 } 171 172 contentType := w.Header().Get("Content-Type") 173 if contentType != "application/json" { 174 t.Errorf("Content-Type: expected application/json, got %s", contentType) 175 } 176 177 // Verify JSON can be parsed 178 var metadata map[string]interface{} 179 err := json.NewDecoder(w.Body).Decode(&metadata) 180 if err != nil { 181 t.Fatalf("Failed to decode JSON response: %v", err) 182 } 183 184 if metadata["client_id"] != "https://oauth.example.com/oauth-client-metadata.json" { 185 t.Errorf("JSON client_id: expected https://oauth.example.com/oauth-client-metadata.json, got %v", metadata["client_id"]) 186 } 187} 188 189// TestGetSession verifies retrieving sessions through the client. 190func TestGetSession(t *testing.T) { 191 client := NewClient("http://localhost:8181") 192 sessionID := "test-session-123" 193 194 key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 195 session := &Session{ 196 DID: "did:plc:test123", 197 AccessToken: "token123", 198 DPoPKey: key, 199 } 200 201 // Store session 202 err := client.SessionStore.Set(sessionID, session) 203 if err != nil { 204 t.Fatalf("Failed to store session: %v", err) 205 } 206 207 // Retrieve via Client.GetSession 208 retrieved, err := client.GetSession(sessionID) 209 if err != nil { 210 t.Fatalf("GetSession failed: %v", err) 211 } 212 213 if retrieved.DID != session.DID { 214 t.Errorf("DID: expected %s, got %s", session.DID, retrieved.DID) 215 } 216 217 if retrieved.AccessToken != session.AccessToken { 218 t.Errorf("AccessToken: expected %s, got %s", session.AccessToken, retrieved.AccessToken) 219 } 220} 221 222// TestGetSessionNotFound verifies error handling for non-existent sessions. 223func TestGetSessionNotFound(t *testing.T) { 224 client := NewClient("http://localhost:8181") 225 226 _, err := client.GetSession("non-existent") 227 if err != ErrSessionNotFound { 228 t.Errorf("Expected ErrSessionNotFound, got %v", err) 229 } 230} 231 232// TestDeleteSession verifies session deletion through the client. 233func TestDeleteSession(t *testing.T) { 234 client := NewClient("http://localhost:8181") 235 sessionID := "test-session-delete" 236 237 key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 238 session := &Session{ 239 DID: "did:plc:test123", 240 AccessToken: "token123", 241 DPoPKey: key, 242 } 243 244 // Store session 245 client.SessionStore.Set(sessionID, session) 246 247 // Delete via Client.DeleteSession 248 err := client.DeleteSession(sessionID) 249 if err != nil { 250 t.Fatalf("DeleteSession failed: %v", err) 251 } 252 253 // Verify it's gone 254 _, err = client.GetSession(sessionID) 255 if err != ErrSessionNotFound { 256 t.Errorf("Expected ErrSessionNotFound after delete, got %v", err) 257 } 258} 259 260// TestClientWithCustomSessionStore verifies using a custom session store. 261func TestClientWithCustomSessionStore(t *testing.T) { 262 customStore := NewMemorySessionStore() 263 264 // Pre-populate the custom store 265 key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 266 session := &Session{ 267 DID: "did:plc:prepopulated", 268 AccessToken: "prepopulated-token", 269 DPoPKey: key, 270 } 271 customStore.Set("prepopulated-id", session) 272 273 // Create client with custom store 274 client := NewClientWithOptions(ClientOptions{ 275 BaseURL: "http://localhost:8181", 276 SessionStore: customStore, 277 }) 278 279 // Verify client can access pre-populated session 280 retrieved, err := client.GetSession("prepopulated-id") 281 if err != nil { 282 t.Fatalf("Failed to retrieve prepopulated session: %v", err) 283 } 284 285 if retrieved.DID != "did:plc:prepopulated" { 286 t.Errorf("Expected prepopulated DID, got %s", retrieved.DID) 287 } 288} 289 290// TestClientMultipleSessions verifies managing multiple sessions. 291func TestClientMultipleSessions(t *testing.T) { 292 client := NewClient("http://localhost:8181") 293 294 // Create multiple sessions 295 sessionCount := 5 296 sessionIDs := make([]string, sessionCount) 297 298 for i := 0; i < sessionCount; i++ { 299 key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 300 sessionID := GenerateSessionID() 301 sessionIDs[i] = sessionID 302 303 session := &Session{ 304 DID: "did:plc:user" + string(rune(i)), 305 AccessToken: "token-" + string(rune(i)), 306 DPoPKey: key, 307 } 308 309 err := client.SessionStore.Set(sessionID, session) 310 if err != nil { 311 t.Fatalf("Failed to store session %d: %v", i, err) 312 } 313 } 314 315 // Verify all sessions can be retrieved 316 for i, sessionID := range sessionIDs { 317 session, err := client.GetSession(sessionID) 318 if err != nil { 319 t.Errorf("Failed to retrieve session %d: %v", i, err) 320 } 321 322 expectedDID := "did:plc:user" + string(rune(i)) 323 if session.DID != expectedDID { 324 t.Errorf("Session %d: expected DID %s, got %s", i, expectedDID, session.DID) 325 } 326 } 327 328 // Delete one session 329 err := client.DeleteSession(sessionIDs[2]) 330 if err != nil { 331 t.Fatalf("Failed to delete session: %v", err) 332 } 333 334 // Verify it's deleted but others remain 335 _, err = client.GetSession(sessionIDs[2]) 336 if err != ErrSessionNotFound { 337 t.Error("Expected session 2 to be deleted") 338 } 339 340 _, err = client.GetSession(sessionIDs[0]) 341 if err != nil { 342 t.Error("Expected session 0 to still exist") 343 } 344} 345 346// TestClientURLConstruction verifies URL construction for different base URLs. 347func TestClientURLConstruction(t *testing.T) { 348 testCases := []struct { 349 baseURL string 350 expectedClientID string 351 expectedRedirect string 352 }{ 353 { 354 baseURL: "http://localhost:8181", 355 expectedClientID: "http://localhost:8181/oauth-client-metadata.json", 356 expectedRedirect: "http://localhost:8181/callback", 357 }, 358 { 359 baseURL: "https://oauth.example.com", 360 expectedClientID: "https://oauth.example.com/oauth-client-metadata.json", 361 expectedRedirect: "https://oauth.example.com/callback", 362 }, 363 { 364 baseURL: "https://oauth.example.com:8443", 365 expectedClientID: "https://oauth.example.com:8443/oauth-client-metadata.json", 366 expectedRedirect: "https://oauth.example.com:8443/callback", 367 }, 368 { 369 baseURL: "http://192.168.1.100:3000", 370 expectedClientID: "http://192.168.1.100:3000/oauth-client-metadata.json", 371 expectedRedirect: "http://192.168.1.100:3000/callback", 372 }, 373 } 374 375 for _, tc := range testCases { 376 t.Run(tc.baseURL, func(t *testing.T) { 377 client := NewClient(tc.baseURL) 378 379 if client.ClientID != tc.expectedClientID { 380 t.Errorf("ClientID: expected %s, got %s", tc.expectedClientID, client.ClientID) 381 } 382 383 if client.RedirectURI != tc.expectedRedirect { 384 t.Errorf("RedirectURI: expected %s, got %s", tc.expectedRedirect, client.RedirectURI) 385 } 386 }) 387 } 388} 389 390// TestClientScopesCustomization verifies custom scopes are respected. 391func TestClientScopesCustomization(t *testing.T) { 392 customScopes := []string{"read", "write", "admin"} 393 394 client := NewClientWithOptions(ClientOptions{ 395 BaseURL: "http://localhost:8181", 396 Scopes: customScopes, 397 }) 398 399 if len(client.Scopes) != len(customScopes) { 400 t.Errorf("Expected %d scopes, got %d", len(customScopes), len(client.Scopes)) 401 } 402 403 for i, scope := range customScopes { 404 if client.Scopes[i] != scope { 405 t.Errorf("Scope %d: expected %s, got %s", i, scope, client.Scopes[i]) 406 } 407 } 408} 409 410// TestClientNameCustomization verifies custom client name is respected. 411func TestClientNameCustomization(t *testing.T) { 412 customName := "My Awesome Bluesky App" 413 414 client := NewClientWithOptions(ClientOptions{ 415 BaseURL: "http://localhost:8181", 416 ClientName: customName, 417 }) 418 419 if client.ClientName != customName { 420 t.Errorf("ClientName: expected %s, got %s", customName, client.ClientName) 421 } 422 423 // Verify it appears in metadata 424 metadata := client.GetClientMetadata() 425 if metadata["client_name"] != customName { 426 t.Errorf("Metadata client_name: expected %s, got %v", customName, metadata["client_name"]) 427 } 428} 429 430// TestErrNoSession verifies the ErrNoSession error constant exists. 431func TestErrNoSession(t *testing.T) { 432 if ErrNoSession == nil { 433 t.Error("ErrNoSession should not be nil") 434 } 435 436 if ErrNoSession.Error() != "no valid session" { 437 t.Errorf("ErrNoSession message: expected 'no valid session', got %s", ErrNoSession.Error()) 438 } 439} 440 441// TestClientMetadataJSONStructure verifies the exact JSON structure. 442func TestClientMetadataJSONStructure(t *testing.T) { 443 client := NewClient("https://test.example.com") 444 handler := client.ClientMetadataHandler() 445 446 req := httptest.NewRequest("GET", "/client-metadata.json", nil) 447 w := httptest.NewRecorder() 448 handler(w, req) 449 450 var metadata map[string]interface{} 451 json.NewDecoder(w.Body).Decode(&metadata) 452 453 // Verify all required OAuth fields are present 454 requiredFields := []string{ 455 "client_id", 456 "client_name", 457 "redirect_uris", 458 "scope", 459 "grant_types", 460 "response_types", 461 "token_endpoint_auth_method", 462 "application_type", 463 "dpop_bound_access_tokens", 464 } 465 466 for _, field := range requiredFields { 467 if _, exists := metadata[field]; !exists { 468 t.Errorf("Required field missing from metadata: %s", field) 469 } 470 } 471} 472 473// TestClientBaseURLEdgeCases verifies edge cases in BaseURL handling. 474func TestClientBaseURLEdgeCases(t *testing.T) { 475 testCases := []struct { 476 name string 477 inputURL string 478 expectedURL string 479 }{ 480 { 481 name: "No trailing slash", 482 inputURL: "http://localhost:8181", 483 expectedURL: "http://localhost:8181", 484 }, 485 { 486 name: "Single trailing slash", 487 inputURL: "http://localhost:8181/", 488 expectedURL: "http://localhost:8181", 489 }, 490 { 491 name: "HTTPS", 492 inputURL: "https://example.com", 493 expectedURL: "https://example.com", 494 }, 495 { 496 name: "With port and trailing slash", 497 inputURL: "http://localhost:3000/", 498 expectedURL: "http://localhost:3000", 499 }, 500 } 501 502 for _, tc := range testCases { 503 t.Run(tc.name, func(t *testing.T) { 504 client := NewClient(tc.inputURL) 505 if client.BaseURL != tc.expectedURL { 506 t.Errorf("BaseURL: expected %s, got %s", tc.expectedURL, client.BaseURL) 507 } 508 }) 509 } 510} 511 512// TestClientSessionStoreIsolation verifies each client has independent session store. 513func TestClientSessionStoreIsolation(t *testing.T) { 514 client1 := NewClient("http://localhost:8181") 515 client2 := NewClient("http://localhost:8282") 516 517 key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 518 sessionID := "shared-id" 519 520 // Store in client1 521 session1 := &Session{ 522 DID: "did:plc:client1", 523 AccessToken: "token1", 524 DPoPKey: key, 525 } 526 client1.SessionStore.Set(sessionID, session1) 527 528 // Client2 should not see client1's session 529 _, err := client2.GetSession(sessionID) 530 if err != ErrSessionNotFound { 531 t.Error("Expected client2 to not see client1's session") 532 } 533 534 // Store in client2 535 session2 := &Session{ 536 DID: "did:plc:client2", 537 AccessToken: "token2", 538 DPoPKey: key, 539 } 540 client2.SessionStore.Set(sessionID, session2) 541 542 // Each client should see their own session 543 retrieved1, _ := client1.GetSession(sessionID) 544 if retrieved1.DID != "did:plc:client1" { 545 t.Error("Client1 session corrupted") 546 } 547 548 retrieved2, _ := client2.GetSession(sessionID) 549 if retrieved2.DID != "did:plc:client2" { 550 t.Error("Client2 session corrupted") 551 } 552} 553 554// TestApplicationTypeDefault verifies default application_type is "web". 555func TestApplicationTypeDefault(t *testing.T) { 556 client := NewClient("https://example.com") 557 558 if client.ApplicationType != ApplicationTypeWeb { 559 t.Errorf("Expected default ApplicationType to be %q, got %q", ApplicationTypeWeb, client.ApplicationType) 560 } 561 562 metadata := client.GetClientMetadata() 563 if metadata["application_type"] != "web" { 564 t.Errorf("Expected metadata application_type to be 'web', got %v", metadata["application_type"]) 565 } 566} 567 568// TestApplicationTypeWeb verifies explicit web application type. 569func TestApplicationTypeWeb(t *testing.T) { 570 client := NewClientWithOptions(ClientOptions{ 571 BaseURL: "https://example.com", 572 ApplicationType: ApplicationTypeWeb, 573 }) 574 575 if client.ApplicationType != ApplicationTypeWeb { 576 t.Errorf("Expected ApplicationType to be %q, got %q", ApplicationTypeWeb, client.ApplicationType) 577 } 578 579 metadata := client.GetClientMetadata() 580 if metadata["application_type"] != "web" { 581 t.Errorf("Expected metadata application_type to be 'web', got %v", metadata["application_type"]) 582 } 583} 584 585// TestApplicationTypeNative verifies native application type. 586func TestApplicationTypeNative(t *testing.T) { 587 client := NewClientWithOptions(ClientOptions{ 588 BaseURL: "myapp://oauth", 589 ApplicationType: ApplicationTypeNative, 590 }) 591 592 if client.ApplicationType != ApplicationTypeNative { 593 t.Errorf("Expected ApplicationType to be %q, got %q", ApplicationTypeNative, client.ApplicationType) 594 } 595 596 metadata := client.GetClientMetadata() 597 if metadata["application_type"] != "native" { 598 t.Errorf("Expected metadata application_type to be 'native', got %v", metadata["application_type"]) 599 } 600} 601 602// TestApplicationTypeInvalid verifies invalid application_type defaults to "web" with warning. 603func TestApplicationTypeInvalid(t *testing.T) { 604 client := NewClientWithOptions(ClientOptions{ 605 BaseURL: "https://example.com", 606 ApplicationType: "invalid_type", 607 }) 608 609 // Should default to web 610 if client.ApplicationType != ApplicationTypeWeb { 611 t.Errorf("Expected invalid ApplicationType to default to %q, got %q", ApplicationTypeWeb, client.ApplicationType) 612 } 613 614 metadata := client.GetClientMetadata() 615 if metadata["application_type"] != "web" { 616 t.Errorf("Expected metadata application_type to default to 'web', got %v", metadata["application_type"]) 617 } 618} 619 620// TestApplicationTypeEmpty verifies empty application_type defaults to "web". 621func TestApplicationTypeEmpty(t *testing.T) { 622 client := NewClientWithOptions(ClientOptions{ 623 BaseURL: "https://example.com", 624 ApplicationType: "", 625 }) 626 627 if client.ApplicationType != ApplicationTypeWeb { 628 t.Errorf("Expected empty ApplicationType to default to %q, got %q", ApplicationTypeWeb, client.ApplicationType) 629 } 630 631 metadata := client.GetClientMetadata() 632 if metadata["application_type"] != "web" { 633 t.Errorf("Expected metadata application_type to default to 'web', got %v", metadata["application_type"]) 634 } 635} 636 637// TestApplicationTypeConstants verifies constant values. 638func TestApplicationTypeConstants(t *testing.T) { 639 if ApplicationTypeWeb != "web" { 640 t.Errorf("Expected ApplicationTypeWeb constant to be 'web', got %q", ApplicationTypeWeb) 641 } 642 643 if ApplicationTypeNative != "native" { 644 t.Errorf("Expected ApplicationTypeNative constant to be 'native', got %q", ApplicationTypeNative) 645 } 646} 647 648// TestPutRecordNoSession verifies PutRecord fails without a valid session. 649func TestPutRecordNoSession(t *testing.T) { 650 client := NewClient("http://localhost:8181") 651 ctx := context.Background() 652 653 // Test with nil session 654 _, err := client.PutRecord(ctx, nil, "com.example.test", "test-rkey", map[string]interface{}{ 655 "text": "test", 656 }) 657 if err != ErrNoSession { 658 t.Errorf("Expected ErrNoSession, got %v", err) 659 } 660 661 // Test with empty access token 662 session := &Session{ 663 DID: "did:plc:test123", 664 AccessToken: "", 665 } 666 _, err = client.PutRecord(ctx, session, "com.example.test", "test-rkey", map[string]interface{}{ 667 "text": "test", 668 }) 669 if err != ErrNoSession { 670 t.Errorf("Expected ErrNoSession for empty access token, got %v", err) 671 } 672} 673 674// TestPutRecordWithSwapNoSession verifies PutRecordWithSwap fails without a valid session. 675func TestPutRecordWithSwapNoSession(t *testing.T) { 676 client := NewClient("http://localhost:8181") 677 ctx := context.Background() 678 679 // Test with nil session 680 _, err := client.PutRecordWithSwap(ctx, nil, "com.example.test", "test-rkey", map[string]interface{}{ 681 "text": "test", 682 }, "swap-cid", "") 683 if err != ErrNoSession { 684 t.Errorf("Expected ErrNoSession, got %v", err) 685 } 686} 687 688// TestListRecordsNoSession verifies ListRecords fails without a valid session. 689func TestListRecordsNoSession(t *testing.T) { 690 client := NewClient("http://localhost:8181") 691 ctx := context.Background() 692 693 // Test with nil session 694 _, err := client.ListRecords(ctx, nil, "com.example.test", nil) 695 if err != ErrNoSession { 696 t.Errorf("Expected ErrNoSession, got %v", err) 697 } 698 699 // Test with empty access token 700 session := &Session{ 701 DID: "did:plc:test123", 702 AccessToken: "", 703 } 704 _, err = client.ListRecords(ctx, session, "com.example.test", nil) 705 if err != ErrNoSession { 706 t.Errorf("Expected ErrNoSession for empty access token, got %v", err) 707 } 708} 709 710// TestListRecordsOptionsStruct verifies ListRecordsOptions fields. 711func TestListRecordsOptionsStruct(t *testing.T) { 712 opts := &ListRecordsOptions{ 713 Repo: "did:plc:other", 714 Limit: 50, 715 Cursor: "cursor123", 716 Reverse: true, 717 } 718 719 if opts.Repo != "did:plc:other" { 720 t.Errorf("Expected Repo 'did:plc:other', got %s", opts.Repo) 721 } 722 723 if opts.Limit != 50 { 724 t.Errorf("Expected Limit 50, got %d", opts.Limit) 725 } 726 727 if opts.Cursor != "cursor123" { 728 t.Errorf("Expected Cursor 'cursor123', got %s", opts.Cursor) 729 } 730 731 if !opts.Reverse { 732 t.Error("Expected Reverse to be true") 733 } 734} 735 736// TestListRecordsResultStruct verifies ListRecordsResult fields. 737func TestListRecordsResultStruct(t *testing.T) { 738 result := &ListRecordsResult{ 739 Records: []RecordEntry{ 740 { 741 URI: "at://did:plc:test/com.example.test/rkey1", 742 CID: "bafyrei123", 743 Value: map[string]interface{}{"text": "test1"}, 744 }, 745 { 746 URI: "at://did:plc:test/com.example.test/rkey2", 747 CID: "bafyrei456", 748 Value: map[string]interface{}{"text": "test2"}, 749 }, 750 }, 751 Cursor: "next-cursor", 752 } 753 754 if len(result.Records) != 2 { 755 t.Errorf("Expected 2 records, got %d", len(result.Records)) 756 } 757 758 if result.Records[0].URI != "at://did:plc:test/com.example.test/rkey1" { 759 t.Errorf("Unexpected first record URI: %s", result.Records[0].URI) 760 } 761 762 if result.Cursor != "next-cursor" { 763 t.Errorf("Expected cursor 'next-cursor', got %s", result.Cursor) 764 } 765} 766 767// TestRecordEntryStruct verifies RecordEntry fields. 768func TestRecordEntryStruct(t *testing.T) { 769 entry := RecordEntry{ 770 URI: "at://did:plc:test/com.example.test/rkey", 771 CID: "bafyrei789", 772 Value: map[string]interface{}{"text": "hello"}, 773 } 774 775 if entry.URI != "at://did:plc:test/com.example.test/rkey" { 776 t.Errorf("Unexpected URI: %s", entry.URI) 777 } 778 779 if entry.CID != "bafyrei789" { 780 t.Errorf("Unexpected CID: %s", entry.CID) 781 } 782 783 valueMap, ok := entry.Value.(map[string]interface{}) 784 if !ok { 785 t.Fatal("Expected Value to be map[string]interface{}") 786 } 787 788 if valueMap["text"] != "hello" { 789 t.Errorf("Expected text 'hello', got %v", valueMap["text"]) 790 } 791} 792 793// TestAuditEventRecordPut verifies the audit event constant for put record. 794func TestAuditEventRecordPut(t *testing.T) { 795 if AuditEventRecordPut != "api.record.put" { 796 t.Errorf("Expected AuditEventRecordPut to be 'api.record.put', got %s", AuditEventRecordPut) 797 } 798} 799 800// TestAuditEventRecordList verifies the audit event constant for list records. 801func TestAuditEventRecordList(t *testing.T) { 802 if AuditEventRecordList != "api.record.list" { 803 t.Errorf("Expected AuditEventRecordList to be 'api.record.list', got %s", AuditEventRecordList) 804 } 805}