Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223import { afterEach, describe, expect, it, vi } from "vitest";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { ClientResponseError } from "@atcute/client";import oauthMetadata from "$lib/oauth-client-metadata";import { missingPermissions } from "$lib/auth/scopes";import { atUriOf, cidOf, didOf, knotOf, nsidOf, recordKeyOf } from "$lib/api/syntax";import type * as Cob from "$lib/api/cob";
const agent = { sub: "did:plc:limpet" } as unknown as OAuthUserAgent;const knot = knotOf("knot.example.com");const repo = didOf("did:plc:barnacle");const collection = nsidOf("org.tangled.track.ticket");const record = { $type: "org.tangled.track.ticket", title: { text: "kelp" } };const uri = atUriOf("at://did:plc:barnacle/org.tangled.track.ticket/3lk");const readCid = "bafyreiread" + "a".repeat(48);const storedCid = "bafyreistored" + "a".repeat(46);const written = { uri, cid: "bafyreiknot" + "a".repeat(48) };const from = { uri, cid: cidOf(readCid) };
const load = async (...responses: (() => Response)[]) => { vi.resetModules(); const minted: { aud: string; lxm: string }[] = []; vi.doMock("$app/environment", () => ({ browser: true })); vi.doMock("$lib/auth/agent", () => ({ mintServiceAuth: async (_agent: unknown, { aud, lxm }: { aud: string; lxm: string }) => { minted.push({ aud, lxm }); return `token-for-${aud}`; } })); const cob = (await import("$lib/api/cob")) as typeof Cob; const cache = await import("$lib/api/cache"); const calls: { url: string; init: RequestInit }[] = []; vi.stubGlobal("fetch", async (input: URL | RequestInfo, init?: RequestInit) => { calls.push({ url: String(input), init: init ?? {} }); return (responses.shift() ?? (() => Response.json(written)))(); }); const sent = (index = 0) => JSON.parse(String(calls[index].init.body)); return { cob, cache, minted, calls, sent };};
afterEach(() => { vi.unstubAllGlobals();});
describe("cob writes go to the repo's knot", () => { it("creates without an rkey and returns what the knot minted", async () => { const { cob, minted, calls, sent } = await load();
const result = await cob.createCob(agent, knot, repo, collection, record);
expect(result).toEqual(written); expect(calls[0].url).toBe(`https://${knot}/xrpc/com.atproto.repo.createRecord`); expect(sent()).toEqual({ repo, collection, record }); expect(minted).toEqual([{ aud: repo, lxm: "com.atproto.repo.createRecord" }]); expect(new Headers(calls[0].init.headers).get("authorization")).toBe( `Bearer token-for-${repo}` ); });
it("sends the rkey when the caller picks one", async () => { const { cob, sent } = await load(); const label = { $type: "org.tangled.label.definition", name: "bug" };
await cob.createCob( agent, knot, repo, nsidOf("org.tangled.label.definition"), label, recordKeyOf("bug") );
expect(sent()).toEqual({ repo, collection: "org.tangled.label.definition", rkey: "bug", record: label }); });
it("edits the record the ref names, against its cid", async () => { const { cob, minted, sent } = await load();
await cob.editCob(agent, knot, from, record);
expect(sent()).toEqual({ repo, collection, rkey: "3lk", swapRecord: readCid, record }); expect(minted[0]).toEqual({ aud: repo, lxm: "com.atproto.repo.putRecord" }); });
it("erases the record the ref names, against its cid", async () => { const { cob, minted, sent } = await load(() => Response.json({ commit: {} }));
await cob.eraseCob(agent, knot, from);
expect(sent()).toEqual({ repo, collection, rkey: "3lk", swapRecord: readCid }); expect(minted[0].lxm).toBe("com.atproto.repo.deleteRecord"); });
it("drops cached reads after a write lands", async () => { const { cob, cache } = await load(); const read = vi.fn().mockResolvedValue("stale");
await cache.cached("issues:did:plc:barnacle:open:", 30_000, read); await cob.createCob(agent, knot, repo, collection, record); await cache.cached("issues:did:plc:barnacle:open:", 30_000, read);
expect(read).toHaveBeenCalledTimes(2); });
it("surfaces the knot's refusal and keeps the cache", async () => { const { cob, cache } = await load(() => Response.json({ error: "Forbidden", message: "policy leaves you out" }, { status: 403 }) ); const read = vi.fn().mockResolvedValue("fresh");
await cache.cached("issues:did:plc:barnacle:open:", 30_000, read); const thrown = await cob.createCob(agent, knot, repo, collection, record).catch((e) => e); await cache.cached("issues:did:plc:barnacle:open:", 30_000, read);
expect(thrown).toBeInstanceOf(ClientResponseError); expect((thrown as ClientResponseError).error).toBe("Forbidden"); expect(read).toHaveBeenCalledTimes(1); });
it("won't make a ref from a view that's missing its cid", async () => { const { cob } = await load();
expect(() => cob.cobRefOf({ uri })).toThrow(); expect(cob.cobRefOf({ uri, cid: readCid })).toEqual(from); });
it("won't build refs from views whose CIDs aren't valid CIDs", async () => { const { cob } = await load();
expect(() => cob.cobRefOf({ uri, cid: "not-a-cid" })).toThrow( '"not-a-cid" is not a valid CID.' ); });
it("uploads blobs with caller-supplied content type and parses returned blobjects", async () => { const { cob, minted, calls } = await load(() => Response.json({ blob: { $type: "blob", ref: { $link: "bafyreiblobaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, mimeType: "image/png", size: 9 } }) );
const receipt = await cob.uploadBlobToKnot( agent, knot, repo, new Uint8Array([1, 2, 3]), "image/png" );
expect(calls[0].url).toBe(`https://${knot}/xrpc/com.atproto.repo.uploadBlob`); expect(calls[0].init.body).toEqual(new Uint8Array([1, 2, 3])); expect(new Headers(calls[0].init.headers).get("content-type")).toBe("image/png"); expect(minted).toEqual([{ aud: repo, lxm: "com.atproto.repo.uploadBlob" }]); expect(receipt).toEqual({ $type: "blob", ref: { $link: "bafyreiblob" + "a".repeat(48) }, mimeType: "image/png", size: 9 }); });
it("accepts validated origin strings in place of URLs", async () => { const { cob, calls } = await load(); await cob.createCob(agent, knotOf("knot.example.com"), repo, collection, record); expect(calls[0].url).toBe("https://knot.example.com/xrpc/com.atproto.repo.createRecord"); });});
describe("updateCob edits the version the knot holds", () => { const stored = { uri, cid: storedCid, value: { ...record, body: "old", "x-tngl-editor": "did:plc:limpet" } };
it("reads from the knot, drops the editor field and swaps against the read cid", async () => { const { cob, calls, sent } = await load( () => Response.json(stored), () => Response.json(written) );
const result = await cob.updateCob( agent, knot, uri, (current: typeof record & { body?: string }) => ({ ...current, body: "new" }) );
expect(calls[0].url).toBe( `https://${knot}/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Abarnacle&collection=org.tangled.track.ticket&rkey=3lk` ); expect(calls[0].init.headers).not.toHaveProperty("authorization"); expect(sent(1)).toEqual({ repo, collection, rkey: "3lk", swapRecord: storedCid, record: { ...record, body: "new" } }); expect(result).toEqual({ ...written, value: { ...record, body: "new" } }); });});
describe("the client metadata covers writes addressed to a repo did", () => { it.each(["createRecord", "putRecord", "deleteRecord"])("grants %s", (method) => { const wanted = [ { resource: "rpc", lxm: nsidOf(`com.atproto.repo.${method}`), aud: repo } ] as const;
expect(missingPermissions(oauthMetadata.scope, wanted)).toEqual([]); });});