~james/core

core/web/src/lib/api/invite.test.ts -rw-r--r-- 3.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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([]);
	});
});