~james/core

core/web/src/lib/api/emails.test.ts -rw-r--r-- 4.5 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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { describe, expect, it, vi, type Mock } from "vitest";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import {
	addEmail,
	deleteEmail,
	listEmails,
	markPrimary,
	removeEmailRow,
	setPrimaryEmail,
	verifyEmailByToken,
	type ListEmailItem
} from "#lib/api/emails.ts";
import { createDeliberiClient } from "#lib/api/deliberi.ts";
import type { Fetch } from "#lib/fetch.ts";

vi.mock("#lib/auth/agent.ts", () => ({
	mintServiceAuth: vi.fn(async () => "test-token"),
	serviceDidForHost: (host: string) => `did:web:${host}`
}));

const row = (overrides: Partial<ListEmailItem> = {}): ListEmailItem => ({
	address: "alice@example.com",
	verified: true,
	primary: false,
	createdAt: "2026-01-01T00:00:00.000Z",
	...overrides
});

const makeCtx = (fetchMock: Fetch) =>
	createDeliberiClient({
		deliberiUrl: "https://deliberi.test",
		agent: {} as unknown as OAuthUserAgent,
		fetch: fetchMock
	});

const calledInit = (mock: Mock<Fetch>) => mock.mock.calls[0][1] as RequestInit;

const jsonResponse = (body: unknown): Response =>
	new Response(JSON.stringify(body), {
		status: 200,
		headers: { "content-type": "application/json" }
	});

describe("listEmails", () => {
	it("gets the listEmails nsid and parses the email shape", async () => {
		const item = row({ address: "bob@example.com", verified: false, primary: true });
		const fetchMock = vi.fn<Fetch>().mockResolvedValue(jsonResponse({ emails: [item] }));
		const out = await listEmails(makeCtx(fetchMock));
		expect(new URL(String(fetchMock.mock.calls[0][0])).pathname).toBe(
			"/xrpc/org.tangled.temp.account.listEmails"
		);
		expect(out.emails).toEqual([item]);
	});
});

describe.each([
	["addEmail", "addEmail", { email: "a@b.co" }],
	["deleteEmail", "deleteEmail", { email: "a@b.co" }],
	["setPrimaryEmail", "setPrimaryEmail", { email: "a@b.co" }]
] as const)("%s", (_name, nsid, body) => {
	it(`posts ${JSON.stringify(body)} to org.tangled.temp.account.${nsid}`, async () => {
		const fetchMock = vi.fn<Fetch>().mockResolvedValue(new Response("", { status: 200 }));
		const ctx = makeCtx(fetchMock);

		if (nsid === "addEmail") await addEmail(ctx, body.email);
		else if (nsid === "deleteEmail") await deleteEmail(ctx, body.email);
		else await setPrimaryEmail(ctx, body.email);

		expect(fetchMock).toHaveBeenCalledTimes(1);
		const url = new URL(String(fetchMock.mock.calls[0][0]));
		expect(url.pathname).toBe(`/xrpc/org.tangled.temp.account.${nsid}`);
		expect(url.origin).toBe("https://deliberi.test");
		expect(calledInit(fetchMock).method).toBe("POST");
		expect(JSON.parse(String(calledInit(fetchMock).body))).toEqual(body);
	});
});

describe("verifyEmailByToken", () => {
	it("posts the token to the open verifyEmail endpoint without auth", async () => {
		const fetchMock = vi
			.fn<Fetch>()
			.mockResolvedValue(jsonResponse({ email: "alice@example.com" }));
		const out = await verifyEmailByToken("https://deliberi.test", "abc123", fetchMock);
		expect(out.email).toBe("alice@example.com");
		expect(fetchMock).toHaveBeenCalledTimes(1);
		const url = new URL(String(fetchMock.mock.calls[0][0]));
		expect(url.pathname).toBe("/xrpc/org.tangled.temp.account.verifyEmail");
		expect(calledInit(fetchMock).method).toBe("POST");
		expect(JSON.parse(String(calledInit(fetchMock).body))).toEqual({ token: "abc123" });
		expect(calledInit(fetchMock).headers).not.toHaveProperty("authorization");
	});

	it("classifies a 400 as an invalid link", async () => {
		const fetchMock = vi.fn<Fetch>().mockResolvedValue(new Response("nope", { status: 400 }));
		await expect(
			verifyEmailByToken("https://deliberi.test", "bad", fetchMock)
		).rejects.toMatchObject({ kind: "invalid" });
	});

	it("classifies a 5xx as an unavailable service", async () => {
		const fetchMock = vi.fn<Fetch>().mockResolvedValue(new Response("boom", { status: 500 }));
		await expect(
			verifyEmailByToken("https://deliberi.test", "bad", fetchMock)
		).rejects.toMatchObject({ kind: "unavailable" });
	});
});

describe("markPrimary", () => {
	it("moves the flag onto the target and clears the rest", () => {
		const rows = [row({ address: "a@x.co", primary: true }), row({ address: "b@x.co" })];
		expect(markPrimary(rows, "b@x.co")).toEqual([
			expect.objectContaining({ address: "a@x.co", primary: false }),
			expect.objectContaining({ address: "b@x.co", primary: true })
		]);
	});
});

describe("removeEmailRow", () => {
	it("drops the deleted address and keeps every other row", () => {
		const rows = [row({ address: "a@x.co" }), row({ address: "b@x.co" })];
		expect(removeEmailRow(rows, "a@x.co").map((r) => r.address)).toEqual(["b@x.co"]);
	});
});