Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340import { describe, expect, it, vi, type Mock } from "vitest";import { beginSignup, completeSignup, getSignup, resendSignup, SignupError} from "#lib/api/signup.ts";
const calledInit = (mock: Mock<typeof globalThis.fetch>) => 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<typeof globalThis.fetch>() .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<typeof globalThis.fetch>().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<typeof globalThis.fetch>() .mockResolvedValue(new Response("nope", { status: 409 })); await expect( beginSignup("https://deliberi.test", "a@b.co", "t", fetchMock) ).rejects.toThrow("Unable to send the link (HTTP 409). Try again in a moment."); });});
describe("resendSignup", () => { it("posts the email to the resendSignup nsid without auth", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .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<typeof globalThis.fetch>().mockResolvedValue( new Response( JSON.stringify({ error: "NoPendingSignup", message: "no link is waiting for this email; sign up again" }), { status: 404, headers: { "content-type": "application/json" } } ) ); await expect(resendSignup("https://deliberi.test", "a@b.co", fetchMock)).rejects.toThrow( "No link is waiting for this email; sign up again." ); });});
describe("getSignup", () => { it("queries the token and returns the signup's email", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response(JSON.stringify({ email: "alice@example.com" }), { status: 200, headers: { "content-type": "application/json" } }) ); await expect(getSignup("https://deliberi.test", "tok-123", fetchMock)).resolves.toEqual({ email: "alice@example.com" }); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/org.tangled.temp.account.getSignup"); expect(url.searchParams.get("token")).toBe("tok-123"); });
it("returns the pds the account will live on", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response(JSON.stringify({ email: "alice@example.com", pds: "https://pds.test" }), { status: 200, headers: { "content-type": "application/json" } }) ); await expect(getSignup("https://deliberi.test", "tok-123", fetchMock)).resolves.toEqual({ email: "alice@example.com", pds: "https://pds.test" }); });
it("reports a dead link with its code", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response( JSON.stringify({ error: "InvalidCode", message: "invalid or expired verification link" }), { status: 400, headers: { "content-type": "application/json" } } ) ); await expect(getSignup("https://deliberi.test", "bad", fetchMock)).rejects.toMatchObject({ code: "InvalidCode" }); });
it("rejects a response without an email", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(new Response("{}", { status: 200 })); await expect(getSignup("https://deliberi.test", "tok", fetchMock)).rejects.toBeInstanceOf( SignupError ); });});
describe("completeSignup", () => { it("posts token, username, and password and parses did/handle", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().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", { token: "tok-123", username: "alice", password: "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("sends the oauth request and returns the pds's authorization", async () => { const authorization = { code: "cod-1", state: "sid-1", iss: "https://pds.test" }; const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue( new Response( JSON.stringify({ did: "did:plc:x", handle: "alice.tngl.sh", authorization }), { status: 200, headers: { "content-type": "application/json" } } ) ); const out = await completeSignup( "https://deliberi.test", { token: "tok", username: "alice", password: "password1234", requestUri: "urn:req" }, fetchMock );
expect(out).toEqual({ did: "did:plc:x", handle: "alice.tngl.sh", authorization }); expect(JSON.parse(String(calledInit(fetchMock).body))).toMatchObject({ requestUri: "urn:req" }); });
it("drops a malformed authorization so the page signs in through the pds", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response( JSON.stringify({ did: "did:plc:x", handle: "alice.tngl.sh", authorization: { code: "c" } }), { status: 200, headers: { "content-type": "application/json" } } ) ); await expect( completeSignup( "https://deliberi.test", { token: "tok", username: "alice", password: "password1234", requestUri: "urn:req" }, fetchMock ) ).resolves.toEqual({ did: "did:plc:x", handle: "alice.tngl.sh" }); });
it("surfaces the server's error message", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().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", { token: "bad", username: "alice", password: "password1234" }, fetchMock ) ).rejects.toThrow("Invalid or expired verification link."); });
it("keeps the service's error code", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response( JSON.stringify({ error: "UsernameNotAvailable", message: "this username is already taken" }), { status: 400, headers: { "content-type": "application/json" } } ) ); const failure = completeSignup( "https://deliberi.test", { token: "tok", username: "alice", password: "password1234" }, fetchMock ); await expect(failure).rejects.toBeInstanceOf(SignupError); await expect(failure).rejects.toMatchObject({ code: "UsernameNotAvailable" }); });
it("falls back to the status when the body is not an xrpc error", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(new Response("bad", { status: 400 })); await expect( completeSignup( "https://deliberi.test", { token: "bad", username: "alice", password: "password1234" }, fetchMock ) ).rejects.toThrow("Unable to create the account (HTTP 400). Try again in a moment."); });
it("replaces server failures with a readable sentence", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response( JSON.stringify({ error: "InternalServerError", message: "internal server error" }), { status: 500, headers: { "content-type": "application/json" } } ) ); await expect( beginSignup("https://deliberi.test", "a@b.co", "token", fetchMock) ).rejects.toThrow("Unable to reach Tangled. Try again in a moment."); });
it("surfaces the error code when the payload omits the message", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response(JSON.stringify({ error: "InvalidCode" }), { status: 400, headers: { "content-type": "application/json" } }) ); await expect( completeSignup( "https://deliberi.test", { token: "bad", username: "alice", password: "password1234" }, fetchMock ) ).rejects.toThrow("InvalidCode"); });
it("rejects a 2xx payload missing the handle", async () => { const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue( new Response(JSON.stringify({ did: "did:plc:x" }), { status: 200, headers: { "content-type": "application/json" } }) ); await expect( completeSignup( "https://deliberi.test", { token: "tok", username: "alice", password: "password1234" }, fetchMock ) ).rejects.toThrow("Unable to create the account. Try again in a moment."); });
it("rejects an empty 2xx body instead of leaking a parse error", async () => { const fetchMock = vi .fn<typeof globalThis.fetch>() .mockResolvedValue(new Response("", { status: 200 })); await expect( completeSignup( "https://deliberi.test", { token: "tok", username: "alice", password: "password1234" }, fetchMock ) ).rejects.toThrow("Unable to create the account. Try again in a moment."); });});