Vibe-guided bskyoauth and custom repo example code in Golang 馃 probably not safe to use in prod
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}