import { 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 type { Did, Nsid } from "@atcute/lexicons/syntax"; import type * as Invite from "$lib/api/invite"; const agent = { sub: "did:plc:limpet" } as unknown as OAuthUserAgent; const KNOT = "knot.oyster.cafe"; const CODE = "8f14e45fceea167a5a36dedd4bea2543"; const NSID = "org.tangled.temp.server.redeemInviteCode"; const load = async (respond: () => Response) => { vi.resetModules(); const minted: { aud: string; lxm: string }[] = []; vi.doMock("$lib/auth/agent", () => ({ mintServiceAuth: async (_agent: unknown, { aud, lxm }: { aud: string; lxm: string }) => { minted.push({ aud, lxm }); return `token-for-${aud}`; }, serviceDidForHost: (host: string) => `did:web:${host.replace(/:/g, "%3A")}` })); const invite = (await import("$lib/api/invite")) as typeof Invite; const calls: { url: string; init: RequestInit }[] = []; const fetch = (async (input: URL | RequestInfo, init?: RequestInit) => { calls.push({ url: String(input), init: init ?? {} }); return respond(); }) as typeof globalThis.fetch; return { invite, minted, calls, fetch }; }; const ok = () => new Response(JSON.stringify({}), { status: 200 }); describe("redeemInviteCode presents the code and the identity together", () => { it("posts the code to the knot with a token minted for that knot and method", async () => { const { invite, minted, calls, fetch } = await load(ok); await invite.redeemInviteCode(agent, KNOT, CODE, fetch); expect(minted).toEqual([{ aud: `did:web:${KNOT}`, lxm: NSID }]); expect(calls[0].url).toBe(`https://${KNOT}/xrpc/${NSID}`); expect(calls[0].init.method).toBe("POST"); expect(JSON.parse(String(calls[0].init.body))).toEqual({ code: CODE }); expect(new Headers(calls[0].init.headers).get("authorization")).toBe( `Bearer token-for-did:web:${KNOT}` ); }); it("keeps a port on the knot host and drops any path", async () => { const { invite, calls, minted, fetch } = await load(ok); await invite.redeemInviteCode(agent, `${KNOT}:8443`, CODE, fetch); expect(calls[0].url).toBe(`https://${KNOT}:8443/xrpc/${NSID}`); expect(minted[0].aud).toBe(`did:web:${KNOT}%3A8443`); }); it("a spent code surfaces the knot's own error", async () => { const { invite, fetch } = await load( () => new Response( JSON.stringify({ error: "InviteNotFound", message: "already spent" }), { status: 400, headers: { "content-type": "application/json" } } ) ); const thrown = await invite.redeemInviteCode(agent, KNOT, CODE, fetch).catch((e) => e); expect(thrown).toBeInstanceOf(ClientResponseError); expect((thrown as ClientResponseError).error).toBe("InviteNotFound"); }); }); describe("the client metadata asks for what the invite flow spends", () => { it("grants the redeem method against any knot", () => { const wanted = [ { resource: "rpc", lxm: NSID as Nsid, aud: `did:web:${KNOT}` as Did } ] as const; expect(missingPermissions(oauthMetadata.scope, wanted)).toEqual([]); }); });