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"; import { createDeliberiClient } from "$lib/api/deliberi"; vi.mock("$lib/auth/agent", () => ({ mintServiceAuth: vi.fn(async () => "test-token"), serviceDidForHost: (host: string) => `did:web:${host}` })); const row = (overrides: Partial = {}): ListEmailItem => ({ address: "alice@example.com", verified: true, primary: false, createdAt: "2026-01-01T00:00:00.000Z", ...overrides }); const makeCtx = (fetchMock: typeof globalThis.fetch) => createDeliberiClient({ deliberiUrl: "https://deliberi.test", agent: {} as unknown as OAuthUserAgent, fetch: fetchMock }); const calledInit = (mock: Mock) => 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() .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() .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() .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() .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() .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"]); }); });