···
12
+
"github.com/bluesky-social/indigo/atproto/atclient"
13
+
"github.com/bluesky-social/indigo/atproto/auth/oauth"
14
+
"github.com/bluesky-social/indigo/atproto/syntax"
17
+
// This test suite provides comprehensive unit tests for the PDS client package.
20
+
// - All Client interface methods: 100%
21
+
// - bearerAuth implementation: 100%
22
+
// - Factory function input validation: 100%
23
+
// - NewFromAccessToken: 100%
25
+
// Not covered (requires integration tests with real infrastructure):
26
+
// - NewFromPasswordAuth success path (requires live PDS server)
27
+
// - NewFromOAuthSession success path (requires OAuth infrastructure)
29
+
// The untested code paths involve external dependencies (PDS authentication,
30
+
// OAuth session resumption) which are appropriately tested in E2E/integration tests.
32
+
// TestClientImplementsInterface verifies that client implements the Client interface.
33
+
func TestClientImplementsInterface(t *testing.T) {
34
+
var _ Client = (*client)(nil)
37
+
// TestBearerAuth_DoWithAuth verifies that bearerAuth correctly adds Authorization header.
38
+
func TestBearerAuth_DoWithAuth(t *testing.T) {
44
+
name: "standard token",
45
+
token: "test-access-token-12345",
48
+
name: "token with special characters",
49
+
token: "token.with.dots_and-dashes",
53
+
for _, tt := range tests {
54
+
t.Run(tt.name, func(t *testing.T) {
55
+
// Create a test server that captures the Authorization header
56
+
var capturedHeader string
57
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
58
+
capturedHeader = r.Header.Get("Authorization")
59
+
w.WriteHeader(http.StatusOK)
61
+
defer server.Close()
63
+
// Create bearerAuth instance
64
+
auth := &bearerAuth{token: tt.token}
67
+
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
69
+
t.Fatalf("failed to create request: %v", err)
72
+
// Execute with auth
73
+
client := &http.Client{}
74
+
nsid := syntax.NSID("com.atproto.test")
75
+
_, err = auth.DoWithAuth(client, req, nsid)
77
+
t.Fatalf("DoWithAuth failed: %v", err)
80
+
// Verify Authorization header
81
+
expectedHeader := "Bearer " + tt.token
82
+
if capturedHeader != expectedHeader {
83
+
t.Errorf("Authorization header = %q, want %q", capturedHeader, expectedHeader)
89
+
// TestBearerAuth_ImplementsAuthMethod verifies bearerAuth implements atclient.AuthMethod.
90
+
func TestBearerAuth_ImplementsAuthMethod(t *testing.T) {
91
+
var _ atclient.AuthMethod = (*bearerAuth)(nil)
94
+
// TestNewFromAccessToken validates factory function input validation.
95
+
func TestNewFromAccessToken(t *testing.T) {
105
+
name: "valid inputs",
106
+
host: "https://pds.example.com",
107
+
did: "did:plc:12345",
108
+
accessToken: "test-token",
112
+
name: "empty host",
114
+
did: "did:plc:12345",
115
+
accessToken: "test-token",
117
+
errContains: "host is required",
121
+
host: "https://pds.example.com",
123
+
accessToken: "test-token",
125
+
errContains: "did is required",
128
+
name: "empty access token",
129
+
host: "https://pds.example.com",
130
+
did: "did:plc:12345",
133
+
errContains: "accessToken is required",
141
+
errContains: "host is required",
145
+
for _, tt := range tests {
146
+
t.Run(tt.name, func(t *testing.T) {
147
+
client, err := NewFromAccessToken(tt.host, tt.did, tt.accessToken)
151
+
t.Fatal("expected error, got nil")
153
+
if !strings.Contains(err.Error(), tt.errContains) {
154
+
t.Errorf("error = %q, want contains %q", err.Error(), tt.errContains)
160
+
t.Fatalf("unexpected error: %v", err)
164
+
t.Fatal("expected client, got nil")
167
+
// Verify DID and HostURL methods
168
+
if client.DID() != tt.did {
169
+
t.Errorf("DID() = %q, want %q", client.DID(), tt.did)
171
+
if client.HostURL() != tt.host {
172
+
t.Errorf("HostURL() = %q, want %q", client.HostURL(), tt.host)
178
+
// TestNewFromPasswordAuth validates factory function input validation.
179
+
func TestNewFromPasswordAuth(t *testing.T) {
180
+
tests := []struct {
189
+
name: "empty host",
191
+
handle: "user.bsky.social",
192
+
password: "password",
194
+
errContains: "host is required",
197
+
name: "empty handle",
198
+
host: "https://pds.example.com",
200
+
password: "password",
202
+
errContains: "handle is required",
205
+
name: "empty password",
206
+
host: "https://pds.example.com",
207
+
handle: "user.bsky.social",
210
+
errContains: "password is required",
218
+
errContains: "host is required",
222
+
for _, tt := range tests {
223
+
t.Run(tt.name, func(t *testing.T) {
224
+
ctx := context.Background()
225
+
_, err := NewFromPasswordAuth(ctx, tt.host, tt.handle, tt.password)
229
+
t.Fatal("expected error, got nil")
231
+
if !strings.Contains(err.Error(), tt.errContains) {
232
+
t.Errorf("error = %q, want contains %q", err.Error(), tt.errContains)
237
+
// Note: We don't test success case here because it requires a real PDS
238
+
// Those are covered in integration tests
243
+
// TestNewFromOAuthSession validates factory function input validation.
244
+
func TestNewFromOAuthSession(t *testing.T) {
245
+
ctx := context.Background()
247
+
tests := []struct {
249
+
oauthClient *oauth.ClientApp
250
+
sessionData *oauth.ClientSessionData
255
+
name: "nil oauth client",
257
+
sessionData: &oauth.ClientSessionData{},
259
+
errContains: "oauthClient is required",
262
+
name: "nil session data",
263
+
oauthClient: &oauth.ClientApp{},
266
+
errContains: "sessionData is required",
273
+
errContains: "oauthClient is required",
277
+
for _, tt := range tests {
278
+
t.Run(tt.name, func(t *testing.T) {
279
+
_, err := NewFromOAuthSession(ctx, tt.oauthClient, tt.sessionData)
283
+
t.Fatal("expected error, got nil")
285
+
if !strings.Contains(err.Error(), tt.errContains) {
286
+
t.Errorf("error = %q, want contains %q", err.Error(), tt.errContains)
291
+
// Note: Success case requires proper OAuth setup, tested in integration tests
296
+
// TestClient_DIDAndHostURL verifies DID() and HostURL() return correct values.
297
+
func TestClient_DIDAndHostURL(t *testing.T) {
298
+
expectedDID := "did:plc:test123"
299
+
expectedHost := "https://pds.test.com"
303
+
host: expectedHost,
306
+
if got := c.DID(); got != expectedDID {
307
+
t.Errorf("DID() = %q, want %q", got, expectedDID)
310
+
if got := c.HostURL(); got != expectedHost {
311
+
t.Errorf("HostURL() = %q, want %q", got, expectedHost)
315
+
// TestClient_CreateRecord tests the CreateRecord method with a mock server.
316
+
func TestClient_CreateRecord(t *testing.T) {
317
+
tests := []struct {
321
+
record map[string]any
322
+
serverResponse map[string]any
329
+
name: "successful creation with rkey",
330
+
collection: "social.coves.vote",
331
+
rkey: "3kjzl5kcb2s2v",
332
+
record: map[string]any{
333
+
"$type": "social.coves.vote",
334
+
"subject": "at://did:plc:abc123/social.coves.post/3kjzl5kc",
337
+
serverResponse: map[string]any{
338
+
"uri": "at://did:plc:test/social.coves.vote/3kjzl5kcb2s2v",
339
+
"cid": "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua",
341
+
serverStatus: http.StatusOK,
342
+
wantURI: "at://did:plc:test/social.coves.vote/3kjzl5kcb2s2v",
343
+
wantCID: "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua",
347
+
name: "successful creation without rkey (TID generated)",
348
+
collection: "social.coves.vote",
350
+
record: map[string]any{
351
+
"$type": "social.coves.vote",
352
+
"subject": "at://did:plc:abc123/social.coves.post/3kjzl5kc",
353
+
"direction": "down",
355
+
serverResponse: map[string]any{
356
+
"uri": "at://did:plc:test/social.coves.vote/3kjzl5kcc2a1b",
357
+
"cid": "bafyreihd4q3yqcfvnv5zlp6n4fqzh6z4p4m3mwc7vvr6k2j6y6v2a3b4c5",
359
+
serverStatus: http.StatusOK,
360
+
wantURI: "at://did:plc:test/social.coves.vote/3kjzl5kcc2a1b",
361
+
wantCID: "bafyreihd4q3yqcfvnv5zlp6n4fqzh6z4p4m3mwc7vvr6k2j6y6v2a3b4c5",
365
+
name: "server error",
366
+
collection: "social.coves.vote",
368
+
record: map[string]any{"$type": "social.coves.vote"},
369
+
serverResponse: map[string]any{
370
+
"error": "InvalidRequest",
371
+
"message": "Invalid record",
373
+
serverStatus: http.StatusBadRequest,
378
+
for _, tt := range tests {
379
+
t.Run(tt.name, func(t *testing.T) {
380
+
// Create mock server
381
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
383
+
if r.Method != http.MethodPost {
384
+
t.Errorf("expected POST request, got %s", r.Method)
388
+
expectedPath := "/xrpc/com.atproto.repo.createRecord"
389
+
if r.URL.Path != expectedPath {
390
+
t.Errorf("path = %q, want %q", r.URL.Path, expectedPath)
393
+
// Verify request body
394
+
var payload map[string]any
395
+
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
396
+
t.Fatalf("failed to decode request body: %v", err)
399
+
// Check required fields
400
+
if payload["collection"] != tt.collection {
401
+
t.Errorf("collection = %v, want %v", payload["collection"], tt.collection)
404
+
// Check rkey inclusion
406
+
if payload["rkey"] != tt.rkey {
407
+
t.Errorf("rkey = %v, want %v", payload["rkey"], tt.rkey)
410
+
if _, exists := payload["rkey"]; exists {
411
+
t.Error("rkey should not be included when empty")
416
+
w.Header().Set("Content-Type", "application/json")
417
+
w.WriteHeader(tt.serverStatus)
418
+
json.NewEncoder(w).Encode(tt.serverResponse)
420
+
defer server.Close()
423
+
apiClient := atclient.NewAPIClient(server.URL)
424
+
apiClient.Auth = &bearerAuth{token: "test-token"}
427
+
apiClient: apiClient,
428
+
did: "did:plc:test",
432
+
// Execute CreateRecord
433
+
ctx := context.Background()
434
+
uri, cid, err := c.CreateRecord(ctx, tt.collection, tt.rkey, tt.record)
438
+
t.Fatal("expected error, got nil")
444
+
t.Fatalf("unexpected error: %v", err)
447
+
if uri != tt.wantURI {
448
+
t.Errorf("uri = %q, want %q", uri, tt.wantURI)
451
+
if cid != tt.wantCID {
452
+
t.Errorf("cid = %q, want %q", cid, tt.wantCID)
458
+
// TestClient_DeleteRecord tests the DeleteRecord method with a mock server.
459
+
func TestClient_DeleteRecord(t *testing.T) {
460
+
tests := []struct {
468
+
name: "successful deletion",
469
+
collection: "social.coves.vote",
470
+
rkey: "3kjzl5kcb2s2v",
471
+
serverStatus: http.StatusOK,
475
+
name: "not found error",
476
+
collection: "social.coves.vote",
477
+
rkey: "nonexistent",
478
+
serverStatus: http.StatusNotFound,
482
+
name: "server error",
483
+
collection: "social.coves.vote",
485
+
serverStatus: http.StatusInternalServerError,
490
+
for _, tt := range tests {
491
+
t.Run(tt.name, func(t *testing.T) {
492
+
// Create mock server
493
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
495
+
if r.Method != http.MethodPost {
496
+
t.Errorf("expected POST request, got %s", r.Method)
500
+
expectedPath := "/xrpc/com.atproto.repo.deleteRecord"
501
+
if r.URL.Path != expectedPath {
502
+
t.Errorf("path = %q, want %q", r.URL.Path, expectedPath)
505
+
// Verify request body
506
+
var payload map[string]any
507
+
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
508
+
t.Fatalf("failed to decode request body: %v", err)
511
+
if payload["collection"] != tt.collection {
512
+
t.Errorf("collection = %v, want %v", payload["collection"], tt.collection)
514
+
if payload["rkey"] != tt.rkey {
515
+
t.Errorf("rkey = %v, want %v", payload["rkey"], tt.rkey)
519
+
w.WriteHeader(tt.serverStatus)
520
+
if tt.serverStatus != http.StatusOK {
521
+
w.Header().Set("Content-Type", "application/json")
522
+
json.NewEncoder(w).Encode(map[string]any{
524
+
"message": "Operation failed",
528
+
defer server.Close()
531
+
apiClient := atclient.NewAPIClient(server.URL)
532
+
apiClient.Auth = &bearerAuth{token: "test-token"}
535
+
apiClient: apiClient,
536
+
did: "did:plc:test",
540
+
// Execute DeleteRecord
541
+
ctx := context.Background()
542
+
err := c.DeleteRecord(ctx, tt.collection, tt.rkey)
546
+
t.Fatal("expected error, got nil")
552
+
t.Fatalf("unexpected error: %v", err)
558
+
// TestClient_ListRecords tests the ListRecords method with pagination.
559
+
func TestClient_ListRecords(t *testing.T) {
560
+
tests := []struct {
565
+
serverResponse map[string]any
572
+
name: "successful list with records",
573
+
collection: "social.coves.vote",
576
+
serverResponse: map[string]any{
577
+
"cursor": "next-cursor-123",
578
+
"records": []map[string]any{
580
+
"uri": "at://did:plc:test/social.coves.vote/1",
581
+
"cid": "bafyreiabc123",
582
+
"value": map[string]any{"$type": "social.coves.vote", "direction": "up"},
585
+
"uri": "at://did:plc:test/social.coves.vote/2",
586
+
"cid": "bafyreiabc456",
587
+
"value": map[string]any{"$type": "social.coves.vote", "direction": "down"},
591
+
serverStatus: http.StatusOK,
593
+
wantCursor: "next-cursor-123",
597
+
name: "empty list",
598
+
collection: "social.coves.vote",
601
+
serverResponse: map[string]any{
603
+
"records": []map[string]any{},
605
+
serverStatus: http.StatusOK,
611
+
name: "with cursor pagination",
612
+
collection: "social.coves.vote",
614
+
cursor: "existing-cursor",
615
+
serverResponse: map[string]any{
616
+
"cursor": "final-cursor",
617
+
"records": []map[string]any{
619
+
"uri": "at://did:plc:test/social.coves.vote/3",
620
+
"cid": "bafyreiabc789",
621
+
"value": map[string]any{"$type": "social.coves.vote", "direction": "up"},
625
+
serverStatus: http.StatusOK,
627
+
wantCursor: "final-cursor",
631
+
name: "server error",
632
+
collection: "social.coves.vote",
635
+
serverResponse: map[string]any{"error": "Internal error"},
636
+
serverStatus: http.StatusInternalServerError,
641
+
for _, tt := range tests {
642
+
t.Run(tt.name, func(t *testing.T) {
643
+
// Create mock server
644
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
646
+
if r.Method != http.MethodGet {
647
+
t.Errorf("expected GET request, got %s", r.Method)
651
+
expectedPath := "/xrpc/com.atproto.repo.listRecords"
652
+
if r.URL.Path != expectedPath {
653
+
t.Errorf("path = %q, want %q", r.URL.Path, expectedPath)
656
+
// Verify query parameters
657
+
query := r.URL.Query()
658
+
if query.Get("collection") != tt.collection {
659
+
t.Errorf("collection param = %q, want %q", query.Get("collection"), tt.collection)
662
+
if tt.cursor != "" {
663
+
if query.Get("cursor") != tt.cursor {
664
+
t.Errorf("cursor param = %q, want %q", query.Get("cursor"), tt.cursor)
669
+
w.Header().Set("Content-Type", "application/json")
670
+
w.WriteHeader(tt.serverStatus)
671
+
json.NewEncoder(w).Encode(tt.serverResponse)
673
+
defer server.Close()
676
+
apiClient := atclient.NewAPIClient(server.URL)
677
+
apiClient.Auth = &bearerAuth{token: "test-token"}
680
+
apiClient: apiClient,
681
+
did: "did:plc:test",
685
+
// Execute ListRecords
686
+
ctx := context.Background()
687
+
resp, err := c.ListRecords(ctx, tt.collection, tt.limit, tt.cursor)
691
+
t.Fatal("expected error, got nil")
697
+
t.Fatalf("unexpected error: %v", err)
701
+
t.Fatal("expected response, got nil")
704
+
if len(resp.Records) != tt.wantRecords {
705
+
t.Errorf("records count = %d, want %d", len(resp.Records), tt.wantRecords)
708
+
if resp.Cursor != tt.wantCursor {
709
+
t.Errorf("cursor = %q, want %q", resp.Cursor, tt.wantCursor)
712
+
// Verify record structure if we have records
713
+
if tt.wantRecords > 0 {
714
+
for i, record := range resp.Records {
715
+
if record.URI == "" {
716
+
t.Errorf("record[%d].URI is empty", i)
718
+
if record.CID == "" {
719
+
t.Errorf("record[%d].CID is empty", i)
721
+
if record.Value == nil {
722
+
t.Errorf("record[%d].Value is nil", i)
730
+
// TestClient_GetRecord tests the GetRecord method with a mock server.
731
+
func TestClient_GetRecord(t *testing.T) {
732
+
tests := []struct {
736
+
serverResponse map[string]any
743
+
name: "successful get",
744
+
collection: "social.coves.vote",
745
+
rkey: "3kjzl5kcb2s2v",
746
+
serverResponse: map[string]any{
747
+
"uri": "at://did:plc:test/social.coves.vote/3kjzl5kcb2s2v",
748
+
"cid": "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua",
749
+
"value": map[string]any{
750
+
"$type": "social.coves.vote",
751
+
"subject": "at://did:plc:abc/social.coves.post/123",
755
+
serverStatus: http.StatusOK,
756
+
wantURI: "at://did:plc:test/social.coves.vote/3kjzl5kcb2s2v",
757
+
wantCID: "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua",
761
+
name: "record not found",
762
+
collection: "social.coves.vote",
763
+
rkey: "nonexistent",
764
+
serverResponse: map[string]any{
765
+
"error": "RecordNotFound",
766
+
"message": "Record not found",
768
+
serverStatus: http.StatusNotFound,
772
+
name: "server error",
773
+
collection: "social.coves.vote",
775
+
serverResponse: map[string]any{
776
+
"error": "Internal error",
778
+
serverStatus: http.StatusInternalServerError,
783
+
for _, tt := range tests {
784
+
t.Run(tt.name, func(t *testing.T) {
785
+
// Create mock server
786
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
788
+
if r.Method != http.MethodGet {
789
+
t.Errorf("expected GET request, got %s", r.Method)
793
+
expectedPath := "/xrpc/com.atproto.repo.getRecord"
794
+
if r.URL.Path != expectedPath {
795
+
t.Errorf("path = %q, want %q", r.URL.Path, expectedPath)
798
+
// Verify query parameters
799
+
query := r.URL.Query()
800
+
if query.Get("collection") != tt.collection {
801
+
t.Errorf("collection param = %q, want %q", query.Get("collection"), tt.collection)
803
+
if query.Get("rkey") != tt.rkey {
804
+
t.Errorf("rkey param = %q, want %q", query.Get("rkey"), tt.rkey)
808
+
w.Header().Set("Content-Type", "application/json")
809
+
w.WriteHeader(tt.serverStatus)
810
+
json.NewEncoder(w).Encode(tt.serverResponse)
812
+
defer server.Close()
815
+
apiClient := atclient.NewAPIClient(server.URL)
816
+
apiClient.Auth = &bearerAuth{token: "test-token"}
819
+
apiClient: apiClient,
820
+
did: "did:plc:test",
824
+
// Execute GetRecord
825
+
ctx := context.Background()
826
+
resp, err := c.GetRecord(ctx, tt.collection, tt.rkey)
830
+
t.Fatal("expected error, got nil")
836
+
t.Fatalf("unexpected error: %v", err)
840
+
t.Fatal("expected response, got nil")
843
+
if resp.URI != tt.wantURI {
844
+
t.Errorf("URI = %q, want %q", resp.URI, tt.wantURI)
847
+
if resp.CID != tt.wantCID {
848
+
t.Errorf("CID = %q, want %q", resp.CID, tt.wantCID)
851
+
if resp.Value == nil {
852
+
t.Error("Value is nil")
858
+
// TestTypedErrors_IsAuthError tests the IsAuthError helper function.
859
+
func TestTypedErrors_IsAuthError(t *testing.T) {
860
+
tests := []struct {
866
+
name: "ErrUnauthorized is auth error",
867
+
err: ErrUnauthorized,
871
+
name: "ErrForbidden is auth error",
876
+
name: "ErrNotFound is not auth error",
881
+
name: "ErrBadRequest is not auth error",
882
+
err: ErrBadRequest,
886
+
name: "wrapped ErrUnauthorized is auth error",
887
+
err: errors.New("outer: " + ErrUnauthorized.Error()),
888
+
wantAuth: false, // Plain string wrap doesn't work
891
+
name: "fmt.Errorf wrapped ErrUnauthorized is auth error",
892
+
err: wrapAPIError(&atclient.APIError{StatusCode: 401, Message: "test"}, "op"),
896
+
name: "fmt.Errorf wrapped ErrForbidden is auth error",
897
+
err: wrapAPIError(&atclient.APIError{StatusCode: 403, Message: "test"}, "op"),
906
+
name: "generic error",
907
+
err: errors.New("something else"),
912
+
for _, tt := range tests {
913
+
t.Run(tt.name, func(t *testing.T) {
914
+
got := IsAuthError(tt.err)
915
+
if got != tt.wantAuth {
916
+
t.Errorf("IsAuthError() = %v, want %v", got, tt.wantAuth)
922
+
// TestWrapAPIError tests error wrapping for HTTP status codes.
923
+
func TestWrapAPIError(t *testing.T) {
924
+
tests := []struct {
932
+
name: "nil error returns nil",
938
+
name: "401 maps to ErrUnauthorized",
939
+
err: &atclient.APIError{StatusCode: 401, Name: "AuthRequired", Message: "Not logged in"},
940
+
operation: "createRecord",
941
+
wantTyped: ErrUnauthorized,
944
+
name: "403 maps to ErrForbidden",
945
+
err: &atclient.APIError{StatusCode: 403, Name: "Forbidden", Message: "Access denied"},
946
+
operation: "deleteRecord",
947
+
wantTyped: ErrForbidden,
950
+
name: "404 maps to ErrNotFound",
951
+
err: &atclient.APIError{StatusCode: 404, Name: "NotFound", Message: "Record not found"},
952
+
operation: "getRecord",
953
+
wantTyped: ErrNotFound,
956
+
name: "400 maps to ErrBadRequest",
957
+
err: &atclient.APIError{StatusCode: 400, Name: "InvalidRequest", Message: "Bad input"},
958
+
operation: "createRecord",
959
+
wantTyped: ErrBadRequest,
962
+
name: "500 wraps without typed error",
963
+
err: &atclient.APIError{StatusCode: 500, Name: "InternalError", Message: "Server error"},
964
+
operation: "listRecords",
965
+
wantTyped: nil, // No typed error for 500
968
+
name: "non-APIError wraps normally",
969
+
err: errors.New("network timeout"),
970
+
operation: "createRecord",
975
+
for _, tt := range tests {
976
+
t.Run(tt.name, func(t *testing.T) {
977
+
result := wrapAPIError(tt.err, tt.operation)
981
+
t.Errorf("expected nil, got %v", result)
987
+
t.Fatal("expected error, got nil")
990
+
if tt.wantTyped != nil {
991
+
if !errors.Is(result, tt.wantTyped) {
992
+
t.Errorf("expected errors.Is(%v, %v) to be true", result, tt.wantTyped)
996
+
// Verify operation is included in error message
997
+
if !strings.Contains(result.Error(), tt.operation) {
998
+
t.Errorf("error message %q should contain operation %q", result.Error(), tt.operation)
1004
+
// TestClient_TypedErrors_CreateRecord tests that CreateRecord returns typed errors.
1005
+
func TestClient_TypedErrors_CreateRecord(t *testing.T) {
1006
+
tests := []struct {
1012
+
name: "401 returns ErrUnauthorized",
1013
+
serverStatus: http.StatusUnauthorized,
1014
+
wantErr: ErrUnauthorized,
1017
+
name: "403 returns ErrForbidden",
1018
+
serverStatus: http.StatusForbidden,
1019
+
wantErr: ErrForbidden,
1022
+
name: "400 returns ErrBadRequest",
1023
+
serverStatus: http.StatusBadRequest,
1024
+
wantErr: ErrBadRequest,
1028
+
for _, tt := range tests {
1029
+
t.Run(tt.name, func(t *testing.T) {
1030
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1031
+
w.Header().Set("Content-Type", "application/json")
1032
+
w.WriteHeader(tt.serverStatus)
1033
+
json.NewEncoder(w).Encode(map[string]any{
1034
+
"error": "TestError",
1035
+
"message": "Test error message",
1038
+
defer server.Close()
1040
+
apiClient := atclient.NewAPIClient(server.URL)
1041
+
apiClient.Auth = &bearerAuth{token: "test-token"}
1044
+
apiClient: apiClient,
1045
+
did: "did:plc:test",
1049
+
ctx := context.Background()
1050
+
_, _, err := c.CreateRecord(ctx, "test.collection", "rkey", map[string]any{})
1053
+
t.Fatal("expected error, got nil")
1056
+
if !errors.Is(err, tt.wantErr) {
1057
+
t.Errorf("expected errors.Is(%v, %v) to be true", err, tt.wantErr)
1063
+
// TestClient_TypedErrors_DeleteRecord tests that DeleteRecord returns typed errors.
1064
+
func TestClient_TypedErrors_DeleteRecord(t *testing.T) {
1065
+
tests := []struct {
1071
+
name: "401 returns ErrUnauthorized",
1072
+
serverStatus: http.StatusUnauthorized,
1073
+
wantErr: ErrUnauthorized,
1076
+
name: "403 returns ErrForbidden",
1077
+
serverStatus: http.StatusForbidden,
1078
+
wantErr: ErrForbidden,
1081
+
name: "404 returns ErrNotFound",
1082
+
serverStatus: http.StatusNotFound,
1083
+
wantErr: ErrNotFound,
1087
+
for _, tt := range tests {
1088
+
t.Run(tt.name, func(t *testing.T) {
1089
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1090
+
w.Header().Set("Content-Type", "application/json")
1091
+
w.WriteHeader(tt.serverStatus)
1092
+
json.NewEncoder(w).Encode(map[string]any{
1093
+
"error": "TestError",
1094
+
"message": "Test error message",
1097
+
defer server.Close()
1099
+
apiClient := atclient.NewAPIClient(server.URL)
1100
+
apiClient.Auth = &bearerAuth{token: "test-token"}
1103
+
apiClient: apiClient,
1104
+
did: "did:plc:test",
1108
+
ctx := context.Background()
1109
+
err := c.DeleteRecord(ctx, "test.collection", "rkey")
1112
+
t.Fatal("expected error, got nil")
1115
+
if !errors.Is(err, tt.wantErr) {
1116
+
t.Errorf("expected errors.Is(%v, %v) to be true", err, tt.wantErr)
1122
+
// TestClient_TypedErrors_ListRecords tests that ListRecords returns typed errors.
1123
+
func TestClient_TypedErrors_ListRecords(t *testing.T) {
1124
+
tests := []struct {
1130
+
name: "401 returns ErrUnauthorized",
1131
+
serverStatus: http.StatusUnauthorized,
1132
+
wantErr: ErrUnauthorized,
1135
+
name: "403 returns ErrForbidden",
1136
+
serverStatus: http.StatusForbidden,
1137
+
wantErr: ErrForbidden,
1141
+
for _, tt := range tests {
1142
+
t.Run(tt.name, func(t *testing.T) {
1143
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1144
+
w.Header().Set("Content-Type", "application/json")
1145
+
w.WriteHeader(tt.serverStatus)
1146
+
json.NewEncoder(w).Encode(map[string]any{
1147
+
"error": "TestError",
1148
+
"message": "Test error message",
1151
+
defer server.Close()
1153
+
apiClient := atclient.NewAPIClient(server.URL)
1154
+
apiClient.Auth = &bearerAuth{token: "test-token"}
1157
+
apiClient: apiClient,
1158
+
did: "did:plc:test",
1162
+
ctx := context.Background()
1163
+
_, err := c.ListRecords(ctx, "test.collection", 10, "")
1166
+
t.Fatal("expected error, got nil")
1169
+
if !errors.Is(err, tt.wantErr) {
1170
+
t.Errorf("expected errors.Is(%v, %v) to be true", err, tt.wantErr)