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.ts";
import { missingPermissions } from "#lib/auth/scopes.ts";
import type { Did, Nsid } from "@atcute/lexicons/syntax";
import type * as Invite from "#lib/api/invite.ts";
import type { Fetch } from "#lib/fetch.ts";
const agent = { sub: "did:plc:limpet" } as unknown as OAuthUserAgent;
const KNOT = "knot.example.com";
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.ts", () => ({
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.ts")) 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 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([]);
});
});