import { describe, expect, it, vi, type Mock } from "vitest"; import { beginSignup, completeSignup, resendSignup } from "$lib/api/signup"; const calledInit = (mock: Mock) => mock.mock.calls[0][1] as RequestInit; describe("beginSignup", () => { it("posts email and turnstile token to the beginSignup nsid without auth", async () => { const fetchMock = vi .fn() .mockResolvedValue(new Response("", { status: 200 })); await beginSignup("https://deliberi.test", "alice@example.com", "turnstile-123", fetchMock); expect(fetchMock).toHaveBeenCalledTimes(1); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.origin).toBe("https://deliberi.test"); expect(url.pathname).toBe("/xrpc/org.tangled.temp.account.beginSignup"); expect(calledInit(fetchMock).method).toBe("POST"); expect(JSON.parse(String(calledInit(fetchMock).body))).toEqual({ email: "alice@example.com", turnstileToken: "turnstile-123" }); expect(calledInit(fetchMock).headers).not.toHaveProperty("authorization"); }); it("surfaces the server's error message", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response( JSON.stringify({ error: "EmailAlreadyRegistered", message: "an account already exists for this email" }), { status: 409, headers: { "content-type": "application/json" } } ) ); await expect( beginSignup("https://deliberi.test", "a@b.co", "t", fetchMock) ).rejects.toThrow("an account already exists for this email"); }); it("falls back to the status when the body is not an xrpc error", async () => { const fetchMock = vi .fn() .mockResolvedValue(new Response("nope", { status: 409 })); await expect( beginSignup("https://deliberi.test", "a@b.co", "t", fetchMock) ).rejects.toThrow(/could not begin signup: 409/); }); }); describe("resendSignup", () => { it("posts the email to the resendSignup nsid without auth", async () => { const fetchMock = vi .fn() .mockResolvedValue(new Response("", { status: 200 })); await resendSignup("https://deliberi.test", "alice@example.com", fetchMock); expect(fetchMock).toHaveBeenCalledTimes(1); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.origin).toBe("https://deliberi.test"); expect(url.pathname).toBe("/xrpc/org.tangled.temp.account.resendSignup"); expect(calledInit(fetchMock).method).toBe("POST"); expect(JSON.parse(String(calledInit(fetchMock).body))).toEqual({ email: "alice@example.com" }); expect(calledInit(fetchMock).headers).not.toHaveProperty("authorization"); }); it("surfaces the server's message when nothing is pending", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response( JSON.stringify({ error: "NoPendingSignup", message: "there is no pending signup for this email; start again from the signup page" }), { status: 404, headers: { "content-type": "application/json" } } ) ); await expect(resendSignup("https://deliberi.test", "a@b.co", fetchMock)).rejects.toThrow( "there is no pending signup for this email" ); }); }); describe("completeSignup", () => { it("posts token, username, and password and parses did/handle", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ did: "did:plc:x", handle: "alice.tngl.sh" }), { status: 200, headers: { "content-type": "application/json" } }) ); const out = await completeSignup( "https://deliberi.test", "tok-123", "alice", "password1234", fetchMock ); expect(out).toEqual({ did: "did:plc:x", handle: "alice.tngl.sh" }); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/org.tangled.temp.account.completeSignup"); expect(calledInit(fetchMock).method).toBe("POST"); expect(JSON.parse(String(calledInit(fetchMock).body))).toEqual({ token: "tok-123", username: "alice", password: "password1234" }); expect(calledInit(fetchMock).headers).not.toHaveProperty("authorization"); }); it("surfaces the server's error message", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response( JSON.stringify({ error: "InvalidCode", message: "invalid or expired verification link" }), { status: 400, headers: { "content-type": "application/json" } } ) ); await expect( completeSignup("https://deliberi.test", "bad", "alice", "password1234", fetchMock) ).rejects.toThrow("invalid or expired verification link"); }); it("falls back to the status when the body is not an xrpc error", async () => { const fetchMock = vi .fn() .mockResolvedValue(new Response("bad", { status: 400 })); await expect( completeSignup("https://deliberi.test", "bad", "alice", "password1234", fetchMock) ).rejects.toThrow(/could not complete signup: 400/); }); it("surfaces the error code when the payload omits the message", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ error: "InvalidCode" }), { status: 400, headers: { "content-type": "application/json" } }) ); await expect( completeSignup("https://deliberi.test", "bad", "alice", "password1234", fetchMock) ).rejects.toThrow("InvalidCode"); }); it("rejects a 2xx payload missing the handle", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ did: "did:plc:x" }), { status: 200, headers: { "content-type": "application/json" } }) ); await expect( completeSignup("https://deliberi.test", "tok", "alice", "password1234", fetchMock) ).rejects.toThrow(/invalid response/); }); it("rejects an empty 2xx body instead of leaking a parse error", async () => { const fetchMock = vi .fn() .mockResolvedValue(new Response("", { status: 200 })); await expect( completeSignup("https://deliberi.test", "tok", "alice", "password1234", fetchMock) ).rejects.toThrow(/invalid response/); }); });