Menu
Public repository / 275d4fa11a39ecc30e2caf604d5a0d499910e8db
AkurAI-Mail / src/lib/api.test.ts
Latest change 22202d1bde69be4ace8a299dffd91f8ff24378df - Recover deployed webmail frontend (frontend parity with EC2 a6afffc) by Ólafur Búi Ólafsson
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { api, apiPost, loginUrl } from "./api";
// api.ts is browser code: it talks to the global fetch and redirects via window.location on 401.
// Both globals are replaced with recording fakes for the duration of each test.
type Call = { path: string; init: RequestInit | undefined };
const realFetch = globalThis.fetch;
const realWindow = (globalThis as { window?: unknown }).window;
let calls: Call[];
let location: { pathname: string; href: string };
function stubFetch(status: number, body: unknown = {}) {
globalThis.fetch = (async (path: string | URL | Request, init?: RequestInit) => {
calls.push({ path: String(path), init });
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
}) as typeof fetch;
}
beforeEach(() => {
calls = [];
location = { pathname: "/", href: "" };
(globalThis as { window?: unknown }).window = { location };
});
afterEach(() => {
globalThis.fetch = realFetch;
(globalThis as { window?: unknown }).window = realWindow;
});
describe("api", () => {
test("sends same-origin credentials and returns the parsed JSON body", async () => {
stubFetch(200, { domains: ["example.com"] });
const result = await api("/api/domains");
expect(result).toEqual({ domains: ["example.com"] });
expect(calls).toHaveLength(1);
expect(calls[0]!.path).toBe("/api/domains");
expect(calls[0]!.init).toMatchObject({ credentials: "same-origin" });
});
test("caller options are merged over the defaults", async () => {
stubFetch(200);
await api("/api/x", { method: "DELETE", credentials: "include" });
expect(calls[0]!.init).toMatchObject({ method: "DELETE", credentials: "include" });
});
test("401 sends the browser to the IdP login with the current path as return_to and never settles", async () => {
stubFetch(401);
location.pathname = "/domains";
const outcome = await Promise.race([api("/api/domains").then(() => "settled"), new Promise((resolve) => setTimeout(() => resolve("pending"), 20))]);
expect(outcome).toBe("pending");
expect(location.href).toBe("/auth/login?return_to=%2Fdomains");
});
test("401 on a webmail page also uses /auth/login", async () => {
stubFetch(401);
location.pathname = "/webmail";
void api("/api/webmail");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(location.href).toBe("/auth/login?return_to=%2Fwebmail");
});
test("non-401 errors are not treated as an auth failure", async () => {
stubFetch(500, { error: "boom" });
expect(await api("/api/x")).toEqual({ error: "boom" });
expect(location.href).toBe("");
});
});
describe("apiPost", () => {
test("POSTs the body as JSON with a JSON content type", async () => {
stubFetch(200, { ok: true });
const result = await apiPost("/api/mailboxes", { address: "a@example.com", quota: 5 });
expect(result).toEqual({ ok: true });
const init = calls[0]!.init!;
expect(init.method).toBe("POST");
expect(init.headers).toEqual({ "content-type": "application/json" });
expect(JSON.parse(init.body as string)).toEqual({ address: "a@example.com", quota: 5 });
});
});
describe("loginUrl", () => {
test("encodes the return path", () => {
expect(loginUrl("/webmail?x=1")).toBe("/auth/login?return_to=%2Fwebmail%3Fx%3D1");
});
});