Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { beforeEach, describe, expect, it, vi } from "vitest";import { createBobbinClient } from "$lib/api/client";import { awaitIndexedOrSaved } from "$lib/api/awaitRecord";import { putProfile, uploadProfileAvatar } from "$lib/api/profile";import { formFromProfile, loadProfile, profileFromForm, saveAvatar, writeProfile} from "$lib/api/profileSettings";import type { ProfileRecord } from "$lib/api/records";
vi.mock("$lib/api/profile", () => ({ PROFILE_COLLECTION: "sh.tangled.actor.profile", putProfile: vi.fn(), uploadProfileAvatar: vi.fn()}));vi.mock("$lib/api/awaitRecord", () => ({ awaitIndexedOrSaved: vi.fn() }));
const agent = { sub: "did:plc:me" } as unknown as OAuthUserAgent;const bobbin = createBobbinClient({ serviceUrl: "https://bobbin.test" });const written = { uri: "at://did:plc:me/sh.tangled.actor.profile/self", cid: "bafynew" };const cid = "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a";const avatar: NonNullable<ProfileRecord["avatar"]> = { $type: "blob", ref: { $link: cid }, mimeType: "image/png", size: 12};
const stored: ProfileRecord = { $type: "sh.tangled.actor.profile", bluesky: true, isOrganization: true, description: "Social coding.", pronouns: "they/them", location: "Helsinki", links: ["https://tangled.org/"], pinnedRepositories: ["did:plc:repo"], avatar};
beforeEach(() => { vi.resetAllMocks(); vi.mocked(putProfile).mockResolvedValue(written as never);});
describe("loadProfile", () => { const ctx = (fetch: typeof globalThis.fetch) => createBobbinClient({ serviceUrl: "https://bobbin.test", fetch });
it("returns the record bobbin has", async () => { const fetch = vi.fn<typeof globalThis.fetch>(async () => Response.json({ uri: "at://did:plc:me/sh.tangled.actor.profile/self", cid, value: stored }) ); await expect(loadProfile(ctx(fetch), "did:plc:me")).resolves.toEqual(stored); const url = new URL(String(fetch.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/sh.tangled.actor.getProfile"); expect(url.searchParams.get("actor")).toBe("at://did:plc:me/sh.tangled.actor.profile/self"); });
it("treats a missing record as empty and anything else as an error", async () => { await expect( loadProfile( ctx(async () => Response.json({ error: "NotFound", message: "" }, { status: 404 })), "did:plc:me" ) ).resolves.toBeNull(); await expect( loadProfile( ctx(async () => Response.json({ error: "UpstreamFailure", message: "" }, { status: 502 }) ), "did:plc:me" ) ).rejects.toThrow(); });});
describe("formFromProfile", () => { it("fills the form from the record and pads the link slots", () => { expect(formFromProfile(stored)).toEqual({ description: "Social coding.", pronouns: "they/them", location: "Helsinki", links: ["https://tangled.org/", "", ""], bluesky: true, isOrganization: true }); });
it("starts empty without a record", () => { expect(formFromProfile(null)).toEqual({ description: "", pronouns: "", location: "", links: ["", "", ""], bluesky: false, isOrganization: false }); });});
describe("profileFromForm", () => { it("keeps the fields the form does not edit and drops blanks", () => { const record = profileFromForm( { description: " Updated. ", pronouns: "", location: " ", links: ["tangled.org", "", " "], bluesky: false, isOrganization: true }, stored );
expect(record).toEqual({ $type: "sh.tangled.actor.profile", bluesky: false, isOrganization: true, description: "Updated.", links: ["https://tangled.org/"], pinnedRepositories: ["did:plc:repo"], avatar }); });
it("only writes the organization flag when it is set", () => { expect(profileFromForm(formFromProfile(null), null)).toEqual({ $type: "sh.tangled.actor.profile", bluesky: false }); expect(profileFromForm({ ...formFromProfile(null), isOrganization: true }, null)).toEqual({ $type: "sh.tangled.actor.profile", bluesky: false, isOrganization: true }); });
it("rejects links that are not http urls", () => { expect(() => profileFromForm({ ...formFromProfile(null), links: ["ftp://x.example"] }, null) ).toThrow("Links must use http or https."); });});
describe("writeProfile", () => { it("waits for bobbin to index the written revision", async () => { await expect(writeProfile(bobbin, agent, stored)).resolves.toEqual(stored); expect(putProfile).toHaveBeenCalledWith(agent, stored); expect(awaitIndexedOrSaved).toHaveBeenCalledWith(bobbin, written); });
it("surfaces an indexing failure", async () => { vi.mocked(awaitIndexedOrSaved).mockRejectedValue( new Error("Saved, but the appview has not caught up yet") ); await expect(writeProfile(bobbin, agent, stored)).rejects.toThrow("has not caught up"); });});
describe("saveAvatar", () => { it("uploads the image and writes it into the current record", async () => { const image = new Blob(["png"], { type: "image/png" }); vi.mocked(uploadProfileAvatar).mockResolvedValue(avatar);
const result = await saveAvatar(bobbin, agent, image, { ...stored, avatar: undefined });
expect(uploadProfileAvatar).toHaveBeenCalledWith(agent, image); expect(putProfile).toHaveBeenCalledWith(agent, { ...stored, avatar }); expect(result).toEqual({ ...stored, avatar }); });
it("removes the avatar without uploading", async () => { await saveAvatar(bobbin, agent, null, stored);
expect(uploadProfileAvatar).not.toHaveBeenCalled(); const { avatar: _avatar, ...withoutAvatar } = stored; expect(putProfile).toHaveBeenCalledWith(agent, withoutAvatar); });});