import { describe, expect, it, vi } from "vitest"; import type { Nsid } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; const agent = { sub: "did:plc:writer" } as unknown as OAuthUserAgent; const collection = "sh.tangled.repo.issue" as Nsid; const record = { $type: "sh.tangled.repo.issue", title: "hi" }; // doMock needs the modules loaded after it, so the imports are dynamic const load = async () => { vi.resetModules(); const inputs: Record[] = []; vi.doMock("$app/environment", () => ({ browser: true })); vi.doMock("$lib/auth/agent", () => ({ createClient: () => ({ call: async (_schema: unknown, options: { input: Record }) => { inputs.push(options.input); return { ok: true, data: { uri: "at://did:plc:writer/c/1", cid: "bafy" } }; } }) })); return { cache: await import("./cache"), write: await import("./write"), inputs }; }; describe("record writes", () => { it("drops cached reads so a write is visible on the next read", async () => { const { cache, write } = await load(); const read = vi.fn().mockResolvedValue("stale"); await cache.cached("issues:did:plc:repo:open:", 30_000, read); await write.putRecord(agent, collection, "3lk", record); await cache.cached("issues:did:plc:repo:open:", 30_000, read); expect(read).toHaveBeenCalledTimes(2); }); it.each([ [ "putRecord", (w: any) => w.putRecord(agent, collection, "3lk", record, "bafyread"), "bafyread" ], [ "deleteRecord", (w: any) => w.deleteRecord(agent, collection, "3lk", "bafycreated"), "bafycreated" ] ])("forwards the swap guard on %s", async (_name, op, swapRecord) => { const { write, inputs } = await load(); await op(write); expect(inputs[0]).toMatchObject({ rkey: "3lk", swapRecord }); }); it("takes a minted rkey instead of letting the pds assign one", async () => { const { write, inputs } = await load(); await write.createRecord(agent, collection, record, "3lkminted"); expect(inputs[0]).toMatchObject({ rkey: "3lkminted" }); }); it("keeps the cache when the write fails", async () => { vi.resetModules(); vi.doMock("$app/environment", () => ({ browser: true })); vi.doMock("$lib/auth/agent", () => ({ createClient: () => ({ call: async () => ({ ok: false, data: { error: "InvalidRequest", message: "nope" } }) }) })); const cache = await import("./cache"); const write = await import("./write"); const read = vi.fn().mockResolvedValue("fresh"); await cache.cached("issues:did:plc:repo:open:", 30_000, read); await expect(write.deleteRecord(agent, collection, "3lk")).rejects.toThrow(); await cache.cached("issues:did:plc:repo:open:", 30_000, read); expect(read).toHaveBeenCalledTimes(1); }); }); describe("readRecord", () => { it("returns record value and written metadata on 200", async () => { vi.resetModules(); vi.doMock("$app/environment", () => ({ browser: true })); vi.doMock("$lib/auth/agent", () => ({ createClient: () => ({ call: async () => ({ ok: true, data: { uri: "at://did:plc:writer/c/1", cid: "bafy123", value: { title: "found" } } }) }) })); const write = await import("./write"); const result = await write.readRecord<{ title: string }>(agent, collection, "3lk"); expect(result).toEqual({ value: { title: "found" }, written: { uri: "at://did:plc:writer/c/1", cid: "bafy123" } }); }); it.each(["RecordNotFound", "CouldNotFindRecord"])("returns null on %s", async (error) => { vi.resetModules(); vi.doMock("$app/environment", () => ({ browser: true })); vi.doMock("$lib/auth/agent", () => ({ createClient: () => ({ call: async () => ({ ok: false, data: { error, message: "not found" } }) }) })); const write = await import("./write"); const result = await write.readRecord(agent, collection, "3lk"); expect(result).toBeNull(); }); it("rethrows non-404 errors instead of swallowing into null", async () => { vi.resetModules(); vi.doMock("$app/environment", () => ({ browser: true })); vi.doMock("$lib/auth/agent", () => ({ createClient: () => ({ call: async () => ({ ok: false, status: 500, data: { error: "InternalServerError", message: "pds down" } }) }) })); const write = await import("./write"); await expect(write.readRecord(agent, collection, "3lk")).rejects.toThrow(); }); });