feat: add browser auth shell
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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.",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user