Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303import { 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.ts";import { missingPermissions } from "#lib/auth/scopes.ts";import { atUriOf, cidOf, didOf, knotOf, nsidOf, recordKeyOf } from "#lib/api/syntax.ts";import type * as Cob from "#lib/api/cob.ts";
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 change = "5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8";
const load = async (...responses: (() => Response)[]) => { vi.resetModules(); const minted: { aud: string; lxm: string }[] = []; const attested: { agent: unknown; subject: unknown; knot: unknown; change: unknown }[] = []; vi.doMock("$app/env", () => ({ browser: true })); vi.doMock("#lib/api/attestation.ts", () => ({ attest: (agent: unknown, subject: unknown, knot: unknown, change: unknown) => attested.push({ agent, subject, knot, change }) })); vi.doMock("#lib/auth/agent.ts", () => ({ 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.ts")) as typeof Cob; const cache = await import("#lib/api/cache.ts"); 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, attested };};
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("attests the version the knot made, for creates and edits, not erases", async () => { const edited = { uri, cid: cidOf("bafyreiedit" + "a".repeat(48)) }; const { cob, attested } = await load( () => Response.json({ ...written, "x-tngl-change": change }), () => Response.json(edited), () => Response.json({ commit: {} }) );
await cob.createCob(agent, knot, repo, collection, record); await cob.editCob(agent, knot, from, record); await cob.eraseCob(agent, knot, from);
expect(attested).toStrictEqual([ { agent, subject: written, knot, change }, { agent, subject: edited, knot, change: undefined } ]); });
it("still returns the write when the knot's change id is malformed", async () => { const { cob, attested } = await load(() => Response.json({ ...written, "x-tngl-change": "not-an-oid" }) );
const result = await cob.createCob(agent, knot, repo, collection, record);
expect(result).toEqual(written); expect(attested).toStrictEqual([{ agent, subject: written, knot, change: undefined }]); });
it("attests nothing when the knot refuses the write", async () => { const { cob, attested } = await load(() => Response.json({ error: "Forbidden" }, { status: 403 }) );
await cob.createCob(agent, knot, repo, collection, record).catch(() => {});
expect(attested).toEqual([]); });
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" } }); });
const loaded = { uri, cid: cidOf(storedCid) }; const touch = (current: typeof record) => current;
it("swaps against the version the page loaded", async () => { const { cob, sent } = await load(() => Response.json(stored));
await cob.updateCob(agent, knot, loaded, touch);
expect(sent(1).swapRecord).toBe(storedCid); });
it("sends nothing when the knot moved past the loaded version", async () => { const { cob, calls } = await load(() => Response.json(stored));
await expect(cob.updateCob(agent, knot, from, touch)).rejects.toBeInstanceOf( cob.StaleEditError ); expect(calls).toHaveLength(1); });
it("maps a 409 InvalidSwap to a stale edit and passes other refusals through", async () => { const refusal = (error: string) => () => Response.json({ error, message: "no" }, { status: 409 }); const swapped = await load(() => Response.json(stored), refusal("InvalidSwap")); await expect(swapped.cob.updateCob(agent, knot, loaded, touch)).rejects.toBeInstanceOf( swapped.cob.StaleEditError );
const conflict = await load(() => Response.json(stored), refusal("Conflict")); await expect(conflict.cob.updateCob(agent, knot, loaded, touch)).rejects.toMatchObject({ status: 409, error: "Conflict" }); });});
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([]); });});