114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { HostAuthError, signInWithEmail, signOutHost } from "../src/auth-protocol.js";
|
|
|
|
const apiBaseUrl = "https://ekklesia.example.test";
|
|
|
|
function recordingFetch(response: Response) {
|
|
const calls: Array<{ readonly init: RequestInit | undefined; readonly url: string }> = [];
|
|
const implementation = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
calls.push({ init, url: String(input) });
|
|
return response.clone();
|
|
}) as unknown as typeof fetch;
|
|
return { calls, implementation };
|
|
}
|
|
|
|
describe("browser authentication transport", () => {
|
|
it("posts email/password to the existing endpoint with browser-managed credentials", async () => {
|
|
const { calls, implementation } = recordingFetch(Response.json({ user: {} }));
|
|
const password = "correct horse battery staple";
|
|
|
|
await signInWithEmail(
|
|
{ apiBaseUrl, fetch: implementation },
|
|
{ email: "[email protected]", password },
|
|
);
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0]?.url).toBe(`${apiBaseUrl}/api/auth/sign-in/email`);
|
|
expect(calls[0]?.url).not.toContain(password);
|
|
expect(calls[0]?.init).toMatchObject({
|
|
cache: "no-store",
|
|
credentials: "include",
|
|
method: "POST",
|
|
redirect: "error",
|
|
});
|
|
expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({
|
|
email: "[email protected]",
|
|
password,
|
|
});
|
|
expect(calls[0]?.init?.headers).toEqual({
|
|
accept: "application/json",
|
|
"content-type": "application/json",
|
|
});
|
|
expect(JSON.stringify(calls[0]?.init?.headers).toLowerCase()).not.toContain("origin");
|
|
});
|
|
|
|
it("maps invalid credentials to one account-neutral, response-neutral failure", async () => {
|
|
for (const status of [400, 401]) {
|
|
const response = new Response(
|
|
JSON.stringify({ detail: "user exists at /private/database", email: "[email protected]" }),
|
|
{ status },
|
|
);
|
|
const { implementation } = recordingFetch(response);
|
|
|
|
const error = await signInWithEmail(
|
|
{ apiBaseUrl, fetch: implementation },
|
|
{ email: "[email protected]", password: "wrong-secret" },
|
|
).catch((caught: unknown) => caught);
|
|
|
|
expect(error).toBeInstanceOf(HostAuthError);
|
|
expect(error).toMatchObject({
|
|
code: "AUTH_CREDENTIALS_INVALID",
|
|
message: "Email or password is incorrect.",
|
|
});
|
|
expect(JSON.stringify(error)).not.toContain("[email protected]");
|
|
expect(JSON.stringify(error)).not.toContain("wrong-secret");
|
|
expect((error as Error).message).not.toContain("private");
|
|
}
|
|
});
|
|
|
|
it("sanitizes transport and server failures without retaining credentials or a cause", async () => {
|
|
const password = "do-not-log-this";
|
|
const implementation = vi.fn(async () => {
|
|
throw new Error(`network failed password=${password} /private/socket`);
|
|
}) as unknown as typeof fetch;
|
|
|
|
const error = await signInWithEmail(
|
|
{ apiBaseUrl, fetch: implementation },
|
|
{ email: "[email protected]", password },
|
|
).catch((caught: unknown) => caught);
|
|
|
|
expect(error).toMatchObject({
|
|
code: "AUTH_REQUEST_FAILED",
|
|
message: "Unable to sign in right now.",
|
|
});
|
|
expect("cause" in (error as object)).toBe(false);
|
|
expect(JSON.stringify(error)).not.toContain(password);
|
|
expect((error as Error).message).not.toContain("private");
|
|
});
|
|
|
|
it("posts an empty body to the existing sign-out endpoint", async () => {
|
|
const { calls, implementation } = recordingFetch(Response.json({ success: true }));
|
|
|
|
await signOutHost({ apiBaseUrl, fetch: implementation });
|
|
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0]?.url).toBe(`${apiBaseUrl}/api/auth/sign-out`);
|
|
expect(calls[0]?.init).toMatchObject({
|
|
body: "{}",
|
|
credentials: "include",
|
|
method: "POST",
|
|
});
|
|
});
|
|
|
|
it("uses a sign-out-specific safe error", async () => {
|
|
const { implementation } = recordingFetch(
|
|
new Response("internal session token", { status: 500 }),
|
|
);
|
|
|
|
await expect(signOutHost({ apiBaseUrl, fetch: implementation })).rejects.toMatchObject({
|
|
code: "AUTH_SIGN_OUT_FAILED",
|
|
message: "Unable to sign out right now.",
|
|
});
|
|
});
|
|
});
|