import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { AttestationSubject } from "$lib/api/attestation"; import { atUriOf, cidOf, didOf, gitOidOf, knotOf } from "$lib/api/syntax"; vi.mock("$app/environment", () => ({ browser: true })); const agent = { sub: "did:plc:writer" } as unknown as OAuthUserAgent; const store = new Map(); const fakeStorage = { getItem: (key: string) => store.get(key) ?? null, setItem: (key: string, value: string) => void store.set(key, value), removeItem: (key: string) => void store.delete(key), key: (index: number) => [...store.keys()][index] ?? null, get length() { return store.size; } }; const settledPrefix = "tangled:attestations:settled:did:plc:writer:"; const subject: AttestationSubject = { uri: atUriOf("at://did:web:knot.test:repo/org.tangled.track.ticket/3labc"), cid: cidOf("bafyreimister" + "a".repeat(46)) }; const other: AttestationSubject = { uri: atUriOf(`${subject.uri}2`), cid: cidOf("bafyreimagoo" + "a".repeat(47)) }; interface Written { rkey: string; record: Record; } type PutRecord = (rkey?: string, record?: unknown) => Promise; const loadWith = async (putRecord: PutRecord) => { vi.resetModules(); const writes: Written[] = []; const attempts = { count: 0 }; vi.doMock("$lib/api/write", () => ({ putRecord: vi.fn( async (_agent: unknown, _collection: unknown, rkey: string, record: unknown) => { try { await putRecord(rkey, record); writes.push({ rkey, record: record as Record }); } finally { attempts.count += 1; } } ) })); vi.doMock("$lib/api/cache", () => ({ clear: () => {} })); const attestation = await import("./attestation"); const queued = async (count: number) => { await vi.waitFor(() => { expect(attestation.pendingAttestations(didOf(agent.sub)).length).toBe(count); }); }; return { attestation, writes, attempts, queued }; }; const succeed: PutRecord = async () => undefined; const fail: PutRecord = async () => { throw new Error("The PDS is unreachable."); }; const failFirst = (() => { let first = true; return async (...args: Parameters) => { if (first) { first = false; return fail(...args); } return succeed(...args); }; })(); beforeEach(() => { store.clear(); vi.stubGlobal("localStorage", fakeStorage); }); afterEach(() => { vi.unstubAllGlobals(); }); describe("attest and flushAttestations", () => { it("queues one subject once, retries with its minted rkey, and never requeues settled subjects", async () => { const { attestation, writes, attempts, queued } = await loadWith(failFirst); attestation.attest(agent, subject); await vi.waitFor(() => expect(attempts.count).toBe(1)); attestation.attest(agent, subject); expect(attestation.pendingAttestations(didOf(agent.sub)).length).toBe(1); const failures = await attestation.flushAttestations(agent); expect(failures).toEqual([]); await queued(0); expect(writes).toHaveLength(1); attestation.attest(agent, subject); await queued(0); expect(writes).toHaveLength(1); expect([...store.keys()].filter((key) => key.startsWith(settledPrefix))).toHaveLength(1); }); it("attests a repeated cid under a new change, and still dedupes the same change and changeless replays", async () => { const first = gitOidOf("1".repeat(40)); const reopened = gitOidOf("2".repeat(40)); const { attestation, writes, queued } = await loadWith(succeed); attestation.attest(agent, subject, undefined, first); await queued(0); attestation.attest(agent, subject, undefined, reopened); await queued(0); attestation.attest(agent, subject, undefined, reopened); await queued(0); expect(writes.map((write) => write.record.change)).toEqual([first, reopened]); attestation.attest(agent, other); await queued(0); attestation.attest(agent, other); await queued(0); expect(writes).toHaveLength(3); }); it("reports each failure message, keeps failed entries queued, and survives corrupt rows and full quotas", async () => { store.set("tangled:attestations:did:plc:writer:3lacorrupt", "{not json"); const { attestation, queued } = await loadWith(fail); attestation.attest(agent, subject); await queued(1); const failures = await attestation.flushAttestations(agent); expect(failures[0]).toBe("The PDS is unreachable."); expect(attestation.pendingAttestations(didOf(agent.sub)).length).toBe(1); vi.stubGlobal("localStorage", { ...fakeStorage, setItem: () => { throw new DOMException("Local storage is full.", "QuotaExceededError"); } }); expect(() => attestation.attest(agent, subject)).not.toThrow(); }); it("takes the newest change listCobVersions has for a cid, and writes without one when the listing fails", async () => { const pages = [ { versions: [ { cid: subject.cid, changeId: "1c0ffee1c0ffee1c0ffee1c0ffee1c0ffee1c0ff" } ], cursor: "500" }, { versions: [ { cid: subject.cid, changeId: "2".repeat(40) }, { cid: subject.cid, changeId: "5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8" } ] } ]; const asked: string[] = []; const orders: (string | null)[] = []; const limits: (string | null)[] = []; vi.stubGlobal( "fetch", vi.fn(async (input: URL) => { if (String(input).includes("knot.other")) return { ok: false } as unknown as Response; asked.push(`${new URL(String(input)).searchParams.get("cursor")}`); orders.push(new URL(String(input)).searchParams.get("order")); limits.push(new URL(String(input)).searchParams.get("limit")); return { ok: true, json: async () => pages[asked.length - 1] } as unknown as Response; }) ); const { attestation, writes, queued } = await loadWith(succeed); attestation.attest(agent, subject, knotOf("knot.test")); await queued(0); attestation.attest(agent, other, knotOf("knot.other")); await queued(0); expect(writes[0]?.record.change).toBe("5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8"); expect(writes[1]?.record.change).toBeUndefined(); expect(asked).toEqual(["0", "500"]); expect(orders).toEqual([null, null]); expect(limits).toEqual(["500", "500"]); }); it("writes no change when the walk ends before the newest match could be seen", async () => { const answers = [ { versions: [ { cid: subject.cid, changeId: "1c0ffee1c0ffee1c0ffee1c0ffee1c0ffee1c0ff" } ], cursor: "500" }, undefined ]; vi.stubGlobal( "fetch", vi.fn(async () => { const answer = answers.shift(); return { ok: answer !== undefined, json: async () => answer } as unknown as Response; }) ); const { attestation, writes, queued } = await loadWith(succeed); attestation.attest(agent, subject, knotOf("knot.test")); await queued(0); expect(writes[0]?.record.change).toBeUndefined(); }); it("doesn't write again for a replay that names no change", async () => { const change = gitOidOf("1".repeat(40)); const { attestation, writes, queued } = await loadWith(succeed); attestation.attest(agent, subject, knotOf("knot.test"), change); await queued(0); attestation.attest(agent, subject, knotOf("knot.test")); await queued(0); expect(writes).toHaveLength(1); }); it("writes the change the knot handed over without asking listCobVersions", async () => { const asked = vi.fn(); vi.stubGlobal("fetch", asked); const change = gitOidOf("5d1f0a3c9b8e47d2a1c6f4e8b3d7a2c5e9f1b6d8"); const { attestation, writes, queued } = await loadWith(succeed); attestation.attest(agent, subject, knotOf("knot.test"), change); await queued(0); expect(writes[0]?.record.change).toBe(change); expect(asked).not.toHaveBeenCalled(); }); it("dismissal mid-flush drops queued entries and lets in-flight writes finish", async () => { let go: (() => void) | undefined; const until = new Promise((resolve) => { go = resolve; }); let dispatched = 0; const { attestation, writes, queued } = await loadWith(async () => { dispatched += 1; if (dispatched === 1) await until; return succeed(); }); attestation.attest(agent, subject); await vi.waitFor(() => expect(dispatched).toBe(1)); attestation.attest(agent, other); await queued(2); attestation.dismissAttestations(didOf(agent.sub)); go?.(); await queued(0); await vi.waitFor(() => expect(writes).toHaveLength(1)); }); });