Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166import { describe, expect, it, vi, type Mock } from "vitest";import { ClientResponseError, createBobbinClient, type BobbinContext } from "$lib/api/client";import { jsonGet, rawGet } from "$lib/api/_request";
const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, ...init });
const makeCtx = ( fetchMock: typeof globalThis.fetch, serviceUrl = "https://bobbin.test"): BobbinContext => createBobbinClient({ serviceUrl, fetch: fetchMock });
const fetchedUrl = (mock: Mock<typeof globalThis.fetch>, n = 0): URL => new URL(String(mock.mock.calls[n][0]));
describe("createBobbinClient", () => { it("normalizes trailing slashes off the service url", () => { const ctx = makeCtx(vi.fn(), "https://bobbin.test///"); expect(ctx.serviceUrl).toBe("https://bobbin.test"); });});
describe("jsonGet URL construction", () => { it("appends an array param as repeated keys", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(jsonResponse({ ok: 1 })); await jsonGet(makeCtx(fetchMock), "sh.tangled.repo.getRepos", { repos: ["a", "b"] }); const url = fetchedUrl(fetchMock); expect(url.searchParams.getAll("repos")).toEqual(["a", "b"]); expect(url.search).toBe("?repos=a&repos=b"); });
it("omits undefined params entirely", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(jsonResponse({ ok: 1 })); await jsonGet(makeCtx(fetchMock), "sh.tangled.x", { keep: "yes", drop: undefined }); const url = fetchedUrl(fetchMock); expect(url.searchParams.has("drop")).toBe(false); expect(url.searchParams.get("keep")).toBe("yes"); });
it("resolves the query against the normalized origin", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(jsonResponse({ ok: 1 })); await jsonGet(makeCtx(fetchMock, "https://bobbin.test/"), "sh.tangled.x", { a: "1" }); expect(fetchedUrl(fetchMock).href).toBe("https://bobbin.test/xrpc/sh.tangled.x?a=1"); });});
describe("jsonGet request headers & signal", () => { it("sends accept: application/json and merges caller headers + signal", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(jsonResponse({ ok: 1 })); const controller = new AbortController(); await jsonGet(makeCtx(fetchMock), "sh.tangled.x", undefined, { headers: { "x-custom": "1" }, signal: controller.signal }); const init = fetchMock.mock.calls[0][1] as RequestInit; expect(init.headers).toMatchObject({ accept: "application/json", "x-custom": "1" }); expect(init.signal).toBe(controller.signal); });});
describe("jsonGet responses", () => { it("throws ClientResponseError carrying status + error/description for a JSON error body", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue( jsonResponse({ error: "RecordNotFound", message: "no such repo" }, { status: 404 }) ); const err = await jsonGet(makeCtx(fetchMock), "sh.tangled.x").catch((e: unknown) => e); expect(err).toBeInstanceOf(ClientResponseError); const cre = err as ClientResponseError; expect(cre.status).toBe(404); expect(cre.error).toBe("RecordNotFound"); expect(cre.description).toBe("no such repo"); });
it("falls back to the status line for a non-JSON error body (does not hang)", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response("<html>502 Bad Gateway</html>", { status: 502, statusText: "Bad Gateway" }) ); const err = await jsonGet(makeCtx(fetchMock), "sh.tangled.x").catch((e: unknown) => e); expect(err).toBeInstanceOf(ClientResponseError); const cre = err as ClientResponseError; expect(cre.status).toBe(502); expect(cre.error).toBe("XRPCError"); expect(cre.description).toBe("Bad Gateway"); });});
describe("rawGet", () => { it("throws ClientResponseError on a non-2xx status", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue( jsonResponse({ error: "UpstreamFailed", message: "knot down" }, { status: 502 }) ); const err = await rawGet(makeCtx(fetchMock), "sh.tangled.repo.archive").catch( (e: unknown) => e ); expect(err).toBeInstanceOf(ClientResponseError); expect((err as ClientResponseError).status).toBe(502); });});
describe("service auth aud", () => { const minted = async (nsid: string) => { vi.resetModules(); const calls: { aud: string; lxm: string }[] = []; vi.doMock("$lib/auth/agent", async (original) => ({ ...(await original<typeof import("$lib/auth/agent")>()), mintServiceAuth: async (_agent: unknown, options: { aud: string; lxm: string }) => { calls.push(options); return "jwt"; } })); const { createBobbinClient: create } = await import("$lib/api/client"); const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(jsonResponse({ ok: 1 })); const ctx = create({ serviceUrl: "https://bobbin.test", fetch: fetchMock, agent: { sub: "did:plc:limpet" } as never }); await ctx.fetch(`https://bobbin.test/xrpc/${nsid}`); return { calls, headers: new Headers(fetchMock.mock.calls[0][1]?.headers) }; };
it("pocket preferences are minted for the app, not for whoever serves them", async () => { const { calls, headers } = await minted("com.bad-example.pocket.getPreferences");
expect(calls).toEqual([ { aud: "did:web:tangled.org", lxm: "com.bad-example.pocket.getPreferences" } ]); expect(headers.get("authorization")).toBe("Bearer jwt"); });
it("putting them lands on the same aud", async () => { const { calls } = await minted("com.bad-example.pocket.putPreferences");
expect(calls[0].aud).toBe("did:web:tangled.org"); });
it("every other method is still minted for the service it is sent to", async () => { const { calls } = await minted("org.tangled.temp.search.searchCode");
expect(calls).toEqual([ { aud: "did:web:bobbin.test", lxm: "org.tangled.temp.search.searchCode" } ]); });});