import { afterEach, describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { atUriOf, didOf, gitOidOf, knotOf, nsidOf } from "$lib/api/syntax"; import oauthMetadata from "$lib/oauth-client-metadata"; import { missingPermissions } from "$lib/auth/scopes"; import type * as Pull from "$lib/api/pull"; const agent = { sub: "did:plc:limpet" } as unknown as OAuthUserAgent; const knot = knotOf("knot.example.com"); const repo = didOf("did:plc:barnacle"); const ticketUri = atUriOf("at://did:plc:barnacle/org.tangled.track.ticket/3lk"); const patchUri = atUriOf("at://did:plc:barnacle/org.tangled.review.patch/bafypatch"); const range = { base: gitOidOf("a".repeat(40)), head: gitOidOf("b".repeat(40)) }; const load = async (responses: (() => Response)[]) => { vi.resetModules(); const minted: string[] = []; vi.doMock("$app/environment", () => ({ browser: true })); vi.doMock("$lib/auth/agent", () => ({ mintServiceAuth: async (_agent: unknown, { lxm }: { lxm: string }) => { minted.push(lxm); return "token"; }, serviceDidForHost: (host: string) => `did:web:${host}` })); const pull = (await import("$lib/api/pull")) as typeof Pull; const calls: { url: string; body?: Record }[] = []; vi.stubGlobal("fetch", async (input: URL | RequestInfo, init?: RequestInit) => { calls.push({ url: String(input), body: init?.body && JSON.parse(String(init.body)) }); return (responses.shift() ?? (() => Response.json({})))(); }); return { pull, minted, calls }; }; afterEach(() => { vi.unstubAllGlobals(); }); describe("pull writes on the knot", () => { it("merges a patch through the knot's merge procedure, addressed to the repo", async () => { const { pull, minted, calls } = await load([]); await pull.mergePull(agent, knot, patchUri); expect(calls[0].url).toBe(`https://${knot}/xrpc/sh.tangled.repo.pull.merge`); expect(calls[0].body).toEqual({ repo, rkey: "bafypatch" }); expect(minted).toEqual(["sh.tangled.repo.pull.merge"]); const wanted = [ { resource: "rpc", lxm: nsidOf("sh.tangled.repo.pull.merge"), aud: repo } ] as const; expect(missingPermissions(oauthMetadata.scope, wanted)).toEqual([]); }); it("opens a pull as a ticket on the target branch plus a patch naming it", async () => { const { pull, calls } = await load([ () => Response.json({ uri: ticketUri, cid: "bafyreiticket" + "a".repeat(46) }), () => Response.json({ uri: patchUri, cid: "bafyreipatch" + "a".repeat(47) }) ]); const opened = await pull.openPull( agent, knot, repo, { title: "kelp", body: "" }, "main", range ); expect(opened).toMatchObject({ uri: ticketUri, cid: "bafyreiticket" + "a".repeat(46) }); expect(calls[0].body).toMatchObject({ collection: "org.tangled.track.ticket", record: { title: { text: "kelp" }, state: "open", targetBranch: "main" } }); expect(calls[0].body?.record).not.toHaveProperty("body"); expect(calls[1].body).toMatchObject({ collection: "org.tangled.review.patch", record: { ticket: "3lk", base: { oid: range.base }, head: { oid: range.head } } }); expect(calls[1].body).not.toHaveProperty("rkey"); }); it("takes the ticket back down when the knot refuses its patch", async () => { const { pull, calls } = await load([ () => Response.json({ uri: ticketUri, cid: "bafyreiticket" + "a".repeat(46) }), () => Response.json( { error: "InvalidRequest", message: "the patch's head isn't a commit" }, { status: 400 } ), () => Response.json({ commit: {} }) ]); const thrown = await pull .openPull(agent, knot, repo, { title: "kelp", body: "" }, "main", range) .catch((cause) => cause); expect(thrown).toBeInstanceOf(Error); expect(calls[2].url).toBe(`https://${knot}/xrpc/com.atproto.repo.deleteRecord`); expect(calls[2].body).toEqual({ repo, collection: "org.tangled.track.ticket", rkey: "3lk", swapRecord: "bafyreiticket" + "a".repeat(46) }); }); });