Something went wrong. Try again.
Go boilerplate library for building atproto apps
atproto go
Something went wrong. Try again.
atp client_test.go
16 kB · 563 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564package atp
import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing"
"github.com/bluesky-social/indigo/atproto/atclient" "github.com/bluesky-social/indigo/atproto/syntax")
func TestNewClient(t *testing.T) { did, err := syntax.ParseDID("did:plc:testuser123") if err != nil { t.Fatal(err) }
c := NewClient(nil, did) if c.DID() != did { t.Fatalf("got DID %v, want %v", c.DID(), did) }}
func TestClient_APIClient(t *testing.T) { did, _ := syntax.ParseDID("did:plc:testuser123") c := NewClient(nil, did) if c.APIClient() != nil { t.Fatal("expected nil APIClient") }}
// testDID is the hardcoded DID used in all testClient-created clients.const testDID = "did:plc:testuser"
// testClient creates a Client backed by a test HTTP server that responds to// XRPC endpoints with the given handler.func testClient(t *testing.T, handler http.HandlerFunc) *Client { t.Helper() srv := httptest.NewServer(handler) t.Cleanup(srv.Close)
did, _ := syntax.ParseDID(testDID) api := atclient.NewAPIClient(srv.URL) api.Client = srv.Client() return NewClient(api, did)}
func TestClient_CreateRecord(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("expected POST, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.repo.createRecord" { t.Fatalf("unexpected path: %s", r.URL.Path) }
var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if body["repo"] != testDID { t.Fatalf("unexpected repo: %v", body["repo"]) } if body["collection"] != "app.bsky.feed.post" { t.Fatalf("unexpected collection: %v", body["collection"]) }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "uri": "at://" + testDID + "/app.bsky.feed.post/3jxy", "cid": "bafyabc123", }) })
uri, cid, err := c.CreateRecord(t.Context(), "app.bsky.feed.post", map[string]string{"text": "hello"}) if err != nil { t.Fatal(err) } if uri != "at://"+testDID+"/app.bsky.feed.post/3jxy" { t.Fatalf("unexpected URI: %s", uri) } if cid != "bafyabc123" { t.Fatalf("unexpected CID: %s", cid) }}
func TestClient_CreateRecord_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "InternalServerError", "message": "something went wrong", }) })
_, _, err := c.CreateRecord(t.Context(), "app.bsky.feed.post", map[string]string{}) if err == nil { t.Fatal("expected error") }}
func TestClient_CreateRecordWithRKey(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("expected POST, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.repo.createRecord" { t.Fatalf("unexpected path: %s", r.URL.Path) }
var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if body["rkey"] != "my-custom-key" { t.Fatalf("unexpected rkey: %v", body["rkey"]) }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "uri": "at://" + testDID + "/app.bsky.feed.post/my-custom-key", "cid": "bafyabc123", }) })
uri, cid, err := c.CreateRecordWithRKey(t.Context(), "app.bsky.feed.post", "my-custom-key", map[string]string{"text": "hello"}) if err != nil { t.Fatal(err) } if uri != "at://"+testDID+"/app.bsky.feed.post/my-custom-key" { t.Fatalf("unexpected URI: %s", uri) } if cid != "bafyabc123" { t.Fatalf("unexpected CID: %s", cid) }}
func TestClient_GetRecord(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Fatalf("expected GET, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.repo.getRecord" { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("rkey") != "3jxy" { t.Fatalf("unexpected rkey: %s", r.URL.Query().Get("rkey")) }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "uri": "at://" + testDID + "/app.bsky.feed.post/3jxy", "cid": "bafyabc123", "value": map[string]string{"text": "hello world"}, }) })
rec, err := c.GetRecord(t.Context(), "app.bsky.feed.post", "3jxy") if err != nil { t.Fatal(err) } if rec.URI != "at://"+testDID+"/app.bsky.feed.post/3jxy" { t.Fatalf("unexpected URI: %s", rec.URI) } if rec.CID != "bafyabc123" { t.Fatalf("unexpected CID: %s", rec.CID) } if rec.Value["text"] != "hello world" { t.Fatalf("unexpected value: %v", rec.Value) }}
func TestClient_GetRecord_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{ "error": "RecordNotFound", "message": "record not found", }) })
_, err := c.GetRecord(t.Context(), "app.bsky.feed.post", "nonexistent") if err == nil { t.Fatal("expected error") }}
func TestClient_ListRecords(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Fatalf("expected GET, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.repo.listRecords" { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("collection") != "app.bsky.feed.post" { t.Fatalf("unexpected collection: %s", r.URL.Query().Get("collection")) }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ { "uri": "at://" + testDID + "/app.bsky.feed.post/3jxy", "cid": "cid1", "value": map[string]string{"text": "first"}, }, { "uri": "at://" + testDID + "/app.bsky.feed.post/3kfk", "cid": "cid2", "value": map[string]string{"text": "second"}, }, }, }) })
result, err := c.ListRecords(t.Context(), "app.bsky.feed.post", 0, "") if err != nil { t.Fatal(err) } if len(result.Records) != 2 { t.Fatalf("expected 2 records, got %d", len(result.Records)) } if result.Records[0].URI != "at://"+testDID+"/app.bsky.feed.post/3jxy" { t.Fatalf("unexpected first record URI: %s", result.Records[0].URI) } if result.Cursor != "" { t.Fatalf("expected empty cursor, got %s", result.Cursor) }}
func TestClient_ListRecords_WithLimitAndCursor(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { cursor := r.URL.Query().Get("cursor") limit := r.URL.Query().Get("limit")
w.Header().Set("Content-Type", "application/json") if cursor == "" { json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ {"uri": "at://" + testDID + "/app.bsky.feed.post/3jxy", "cid": "cid1", "value": map[string]string{"text": "first"}}, }, "cursor": "next-page-token", }) } else { json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ {"uri": "at://" + testDID + "/app.bsky.feed.post/3kfk", "cid": "cid2", "value": map[string]string{"text": "second"}}, }, }) }
if limit != "100" { t.Fatalf("expected limit=100, got %s", limit) } })
result, err := c.ListRecords(t.Context(), "app.bsky.feed.post", 100, "") if err != nil { t.Fatal(err) } if len(result.Records) != 1 { t.Fatalf("expected 1 record, got %d", len(result.Records)) } if result.Cursor != "next-page-token" { t.Fatalf("expected cursor next-page-token, got %s", result.Cursor) }
result, err = c.ListRecords(t.Context(), "app.bsky.feed.post", 100, "next-page-token") if err != nil { t.Fatal(err) } if len(result.Records) != 1 { t.Fatalf("expected 1 record on second page, got %d", len(result.Records)) } if result.Cursor != "" { t.Fatalf("expected empty cursor on last page, got %s", result.Cursor) }}
func TestClient_ListRecords_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{ "error": "InvalidRequest", "message": "bad request", }) })
_, err := c.ListRecords(t.Context(), "app.bsky.feed.post", 0, "") if err == nil { t.Fatal("expected error") }}
func TestClient_ListAllRecords(t *testing.T) { pageNum := 0 c := testClient(t, func(w http.ResponseWriter, r *http.Request) { pageNum++ w.Header().Set("Content-Type", "application/json")
if pageNum <= 2 { json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ {"uri": "at://" + testDID + "/app.bsky.feed.post/3jxy", "cid": "cid1", "value": map[string]string{"text": "hello"}}, }, "cursor": "token", }) } else { json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ {"uri": "at://" + testDID + "/app.bsky.feed.post/3kfk", "cid": "cid2", "value": map[string]string{"text": "world"}}, }, }) } })
records, err := c.ListAllRecords(t.Context(), "app.bsky.feed.post") if err != nil { t.Fatal(err) } if len(records) != 3 { t.Fatalf("expected 3 records total, got %d", len(records)) }}
func TestClient_ListAllRecords_Cancel(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ {"uri": "at://" + testDID + "/app.bsky.feed.post/3jxy", "cid": "cid1", "value": map[string]string{"text": "hi"}}, }, "cursor": "keep-going", }) })
ctx, cancel := context.WithCancel(t.Context()) cancel()
_, err := c.ListAllRecords(ctx, "app.bsky.feed.post") if err == nil { t.Fatal("expected context cancellation error") }}
func TestClient_PutRecord(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("expected POST, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.repo.putRecord" { t.Fatalf("unexpected path: %s", r.URL.Path) }
var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if body["rkey"] != "3jxy" { t.Fatalf("unexpected rkey: %v", body["rkey"]) } if body["repo"] != testDID { t.Fatalf("unexpected repo: %v", body["repo"]) }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "uri": "at://" + testDID + "/app.bsky.feed.post/3jxy", "cid": "bafyupdated", }) })
uri, cid, err := c.PutRecord(t.Context(), "app.bsky.feed.post", "3jxy", map[string]string{"text": "updated"}) if err != nil { t.Fatal(err) } if uri != "at://"+testDID+"/app.bsky.feed.post/3jxy" { t.Fatalf("unexpected URI: %s", uri) } if cid != "bafyupdated" { t.Fatalf("unexpected CID: %s", cid) }}
func TestClient_DeleteRecord(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("expected POST, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.repo.deleteRecord" { t.Fatalf("unexpected path: %s", r.URL.Path) }
var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if body["rkey"] != "3jxy" { t.Fatalf("unexpected rkey: %v", body["rkey"]) }
w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]any{}) })
if err := c.DeleteRecord(t.Context(), "app.bsky.feed.post", "3jxy"); err != nil { t.Fatal(err) }}
func TestClient_DeleteRecord_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{ "error": "InternalServerError", "message": "could not delete", }) })
if err := c.DeleteRecord(t.Context(), "app.bsky.feed.post", "3jxy"); err == nil { t.Fatal("expected error") }}
func TestClient_UploadBlob(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("expected POST, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.repo.uploadBlob" { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.Header.Get("Content-Type") != "image/png" { t.Fatalf("unexpected Content-Type: %s", r.Header.Get("Content-Type")) }
w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // LexBlob JSON: ref uses $link format json.NewEncoder(w).Encode(map[string]any{ "blob": map[string]any{ "$type": "blob", "mimeType": "image/png", "size": 12345, "ref": map[string]string{"$link": "bafkreiern4acpjlva5gookrtc534gr4nmuj7pbvfsg6yslnbuv336izv7e"}, }, }) })
blob, err := c.UploadBlob(t.Context(), []byte("fake-image-data"), "image/png") if err != nil { t.Fatal(err) } if blob.Type != "blob" { t.Fatalf("expected type blob, got %s", blob.Type) } if blob.MimeType != "image/png" { t.Fatalf("expected image/png, got %s", blob.MimeType) } if blob.Size != 12345 { t.Fatalf("expected size 12345, got %d", blob.Size) } if blob.Ref.Link != "bafkreiern4acpjlva5gookrtc534gr4nmuj7pbvfsg6yslnbuv336izv7e" { t.Fatalf("unexpected ref link: %s", blob.Ref.Link) }}
func TestClient_UploadBlob_TooLarge(t *testing.T) { did, _ := syntax.ParseDID(testDID) api := atclient.NewAPIClient("http://localhost:9999") c := NewClient(api, did)
_, err := c.UploadBlob(t.Context(), make([]byte, MaxBlobSize+1), "application/octet-stream") if err == nil { t.Fatal("expected error for blob exceeding MaxBlobSize") }}
func TestClient_UploadBlob_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { // Return a non-JSON body so json.Unmarshal into RepoUploadBlob_Output fails w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal Server Error")) })
_, err := c.UploadBlob(t.Context(), []byte("data"), "text/plain") if err == nil { t.Fatal("expected error") }}
func TestClient_GetBlob(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Fatalf("expected GET, got %s", r.Method) } if r.URL.Path != "/xrpc/com.atproto.sync.getBlob" { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("cid") != "bafyblob123" { t.Fatalf("unexpected cid: %s", r.URL.Query().Get("cid")) } if r.URL.Query().Get("did") != testDID { t.Fatalf("unexpected did: %s", r.URL.Query().Get("did")) }
w.Header().Set("Content-Type", "application/octet-stream") w.Write([]byte("fake-blob-data")) })
data, err := c.GetBlob(t.Context(), "bafyblob123") if err != nil { t.Fatal(err) } if string(data) != "fake-blob-data" { t.Fatalf("unexpected blob data: %s", string(data)) }}
func TestClient_GetBlob_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })
_, err := c.GetBlob(t.Context(), "nonexistent") if err == nil { t.Fatal("expected error") }}
func TestClient_CreateRecordWithRKey_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusConflict) json.NewEncoder(w).Encode(map[string]string{ "error": "InvalidRecord", "message": "record already exists", }) })
_, _, err := c.CreateRecordWithRKey(t.Context(), "app.bsky.feed.post", "existing-key", map[string]string{}) if err == nil { t.Fatal("expected error for conflict") }}
func TestClient_PutRecord_Error(t *testing.T) { c := testClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) json.NewEncoder(w).Encode(map[string]string{ "error": "Forbidden", "message": "not authorized to write", }) })
_, _, err := c.PutRecord(t.Context(), "app.bsky.feed.post", "3jxy", map[string]string{}) if err == nil { t.Fatal("expected error") }}