package profile import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/bluesky-social/indigo/atproto/syntax" ) const testDID = "did:plc:abcdefghij1234567890abcd" // newPDS returns a minimal PDS-like test server that serves // com.atproto.repo.getRecord. The handler is supplied by each test. func newPDS(t *testing.T, handler func(http.ResponseWriter, *http.Request)) *httptest.Server { t.Helper() mux := http.NewServeMux() mux.HandleFunc("/xrpc/com.atproto.repo.getRecord", handler) return httptest.NewServer(mux) } func TestFetchBluesky_OK(t *testing.T) { srv := newPDS(t, func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() if q.Get("repo") != testDID { t.Errorf("repo = %q, want %q", q.Get("repo"), testDID) } if q.Get("collection") != "app.bsky.actor.profile" { t.Errorf("collection = %q", q.Get("collection")) } if q.Get("rkey") != "self" { t.Errorf("rkey = %q", q.Get("rkey")) } _ = json.NewEncoder(w).Encode(map[string]any{ "uri": "at://" + testDID + "/app.bsky.actor.profile/self", "cid": "bafyreigh2akiscaildc", "value": map[string]any{ "$type": "app.bsky.actor.profile", "displayName": "Brittany", "description": "frontend nerd ยท atproto enthusiast", "avatar": map[string]any{ "$type": "blob", "ref": map[string]any{"$link": "bafkreiavatar1234"}, "mimeType": "image/jpeg", "size": 123456, }, }, }) }) defer srv.Close() did, _ := syntax.ParseDID(testDID) rec, err := FetchBluesky(context.Background(), srv.URL, did) if err != nil { t.Fatalf("FetchBluesky: %v", err) } if rec.DisplayName != "Brittany" { t.Errorf("DisplayName = %q", rec.DisplayName) } if !strings.Contains(rec.Description, "atproto enthusiast") { t.Errorf("Description = %q", rec.Description) } if rec.Avatar == nil || rec.Avatar.CID() != "bafkreiavatar1234" { t.Errorf("Avatar.CID = %q", rec.Avatar.CID()) } if rec.Avatar.MimeType != "image/jpeg" { t.Errorf("Avatar.MimeType = %q", rec.Avatar.MimeType) } } func TestFetchBluesky_NotFound(t *testing.T) { // PDSes return 400 InvalidRequest with name=RecordNotFound when a record // doesn't exist on a known repo. srv := newPDS(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]any{ "error": "RecordNotFound", "message": "Could not locate record", }) }) defer srv.Close() did, _ := syntax.ParseDID(testDID) _, err := FetchBluesky(context.Background(), srv.URL, did) if !errors.Is(err, ErrNotFound) { t.Fatalf("err = %v, want ErrNotFound", err) } } func TestFetchBluesky_NotFound404(t *testing.T) { // Some implementations might surface as plain 404. srv := newPDS(t, func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) _ = json.NewEncoder(w).Encode(map[string]any{"error": "NotFound"}) }) defer srv.Close() did, _ := syntax.ParseDID(testDID) _, err := FetchBluesky(context.Background(), srv.URL, did) if !errors.Is(err, ErrNotFound) { t.Fatalf("err = %v, want ErrNotFound", err) } } func TestFetchBluesky_OtherError(t *testing.T) { srv := newPDS(t, func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _ = json.NewEncoder(w).Encode(map[string]any{"error": "Boom"}) }) defer srv.Close() did, _ := syntax.ParseDID(testDID) _, err := FetchBluesky(context.Background(), srv.URL, did) if err == nil { t.Fatal("expected error") } if errors.Is(err, ErrNotFound) { t.Fatalf("err should not be ErrNotFound: %v", err) } } func TestFetchQuest_OK(t *testing.T) { srv := newPDS(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("collection") != "quest.atmo.profile" { t.Errorf("collection = %q", r.URL.Query().Get("collection")) } _ = json.NewEncoder(w).Encode(map[string]any{ "uri": "at://" + testDID + "/quest.atmo.profile/self", "cid": "bafyreiquest1", "value": map[string]any{ "$type": "quest.atmo.profile", "bio": "my atmo override bio", "interests": []string{"coffee", "atproto", "hiking"}, "links": []map[string]any{ {"label": "My Site", "url": "https://example.com"}, {"label": "GitHub", "url": "https://github.com/example"}, }, "updatedAt": "2026-05-14T12:34:56Z", }, }) }) defer srv.Close() did, _ := syntax.ParseDID(testDID) rec, err := FetchQuest(context.Background(), srv.URL, did) if err != nil { t.Fatalf("FetchQuest: %v", err) } if rec.Bio != "my atmo override bio" { t.Errorf("Bio = %q", rec.Bio) } if len(rec.Interests) != 3 || rec.Interests[0] != "coffee" { t.Errorf("Interests = %v", rec.Interests) } if len(rec.Links) != 2 || rec.Links[1].URL != "https://github.com/example" { t.Errorf("Links = %+v", rec.Links) } if rec.UpdatedAt.IsZero() { t.Errorf("UpdatedAt should parse, got zero") } } func TestFetchQuest_NotFound(t *testing.T) { srv := newPDS(t, func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _ = json.NewEncoder(w).Encode(map[string]any{ "error": "RecordNotFound", "message": "Could not locate record", }) }) defer srv.Close() did, _ := syntax.ParseDID(testDID) _, err := FetchQuest(context.Background(), srv.URL, did) if !errors.Is(err, ErrNotFound) { t.Fatalf("err = %v, want ErrNotFound", err) } } func TestAvatarURL(t *testing.T) { did, _ := syntax.ParseDID(testDID) got := AvatarURL("https://example-pds.test", did, "bafkreiavatar") u, err := url.Parse(got) if err != nil { t.Fatalf("not a URL: %v", err) } if u.Host != "example-pds.test" { t.Errorf("host = %q", u.Host) } if u.Path != "/xrpc/com.atproto.sync.getBlob" { t.Errorf("path = %q", u.Path) } q := u.Query() if q.Get("did") != testDID { t.Errorf("did param = %q", q.Get("did")) } if q.Get("cid") != "bafkreiavatar" { t.Errorf("cid param = %q", q.Get("cid")) } } func TestAvatarURL_TrailingSlashStripped(t *testing.T) { did, _ := syntax.ParseDID(testDID) got := AvatarURL("https://pds.test/", did, "bafkreiX") if strings.Contains(got, "//xrpc") { t.Errorf("trailing slash not stripped: %s", got) } } func TestAvatarURL_EmptyInputs(t *testing.T) { did, _ := syntax.ParseDID(testDID) if AvatarURL("", did, "cid") != "" { t.Error("empty PDS should return empty string") } if AvatarURL("https://x", did, "") != "" { t.Error("empty CID should return empty string") } if AvatarURL("https://x", "", "cid") != "" { t.Error("empty DID should return empty string") } } func TestBlobRef_CID_Nil(t *testing.T) { var b *BlobRef if b.CID() != "" { t.Error("nil BlobRef.CID should be empty") } } func TestEffectiveBio(t *testing.T) { cases := []struct { name string quest *QuestRecord bsky *BlueskyRecord want string }{ {"override beats bsky", &QuestRecord{Bio: "atmo"}, &BlueskyRecord{Description: "bsky"}, "atmo"}, {"empty override falls back", &QuestRecord{Bio: " "}, &BlueskyRecord{Description: "bsky"}, "bsky"}, {"no quest record uses bsky", nil, &BlueskyRecord{Description: "bsky"}, "bsky"}, {"both empty", nil, nil, ""}, {"both whitespace", &QuestRecord{Bio: " "}, &BlueskyRecord{Description: " "}, ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := EffectiveBio(tc.quest, tc.bsky); got != tc.want { t.Errorf("got %q, want %q", got, tc.want) } }) } }