94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
import {
|
|
createContext,
|
|
hashPassword,
|
|
verifyPassword,
|
|
logIn,
|
|
logOut,
|
|
getUser,
|
|
sessionAuth,
|
|
requireAuth,
|
|
} from "../src/index.ts";
|
|
|
|
function ctx(method = "GET", path = "/", accept?: string) {
|
|
const headers: Record<string, string> = {};
|
|
if (accept) headers.accept = accept;
|
|
const url = new URL(`http://x${path}`);
|
|
const req = new Request(url, { method, headers });
|
|
return createContext(req, url);
|
|
}
|
|
|
|
test("hashPassword / verifyPassword round-trip", async () => {
|
|
const hash = await hashPassword("correct horse battery staple");
|
|
expect(hash).toBeTruthy();
|
|
expect(hash).not.toBe("correct horse battery staple");
|
|
expect(await verifyPassword("correct horse battery staple", hash)).toBe(true);
|
|
expect(await verifyPassword("wrong", hash)).toBe(false);
|
|
});
|
|
|
|
test("verifyPassword tolerates empty/garbage hashes", async () => {
|
|
expect(await verifyPassword("x", "")).toBe(false);
|
|
expect(await verifyPassword("x", "not-a-real-hash")).toBe(false);
|
|
});
|
|
|
|
test("logIn stores the user; getUser reads it; logOut clears it", () => {
|
|
const c = ctx();
|
|
expect(getUser(c)).toBeNull();
|
|
logIn(c, { id: 1, email: "a@b.com" });
|
|
expect(getUser<{ id: number }>(c)?.id).toBe(1);
|
|
expect(c.session.get<{ id: number; email: string }>("user")).toEqual({ id: 1, email: "a@b.com" });
|
|
logOut(c);
|
|
expect(getUser(c)).toBeNull();
|
|
expect(c.user).toBeNull();
|
|
});
|
|
|
|
test("logIn regenerates the session id (fixation defense) but keeps data", () => {
|
|
const c = ctx();
|
|
c.session.set("cart", [1, 2]);
|
|
const before = c.session.id();
|
|
logIn(c, { id: 1, email: "a@b.com" });
|
|
const after = c.session.id();
|
|
expect(after).not.toBe(before); // fresh id issued on login
|
|
expect(after.length).toBeGreaterThanOrEqual(32);
|
|
expect(c.session.get<number[]>("cart")).toEqual([1, 2]); // data preserved
|
|
expect(getUser<{ id: number }>(c)?.id).toBe(1);
|
|
});
|
|
|
|
test("sessionAuth hydrates ctx.user from the session", async () => {
|
|
const c = ctx();
|
|
c.session.set("user", { id: 7 });
|
|
let seen: unknown = "unset";
|
|
await sessionAuth()(c, () => {
|
|
seen = c.user;
|
|
return new Response("ok");
|
|
});
|
|
expect(seen).toEqual({ id: 7 });
|
|
});
|
|
|
|
test("requireAuth: passes through when authenticated", async () => {
|
|
const c = ctx();
|
|
logIn(c, { id: 1 });
|
|
const res = await requireAuth()(c, () => new Response("secret"));
|
|
expect(await res.text()).toBe("secret");
|
|
});
|
|
|
|
test("requireAuth: 401 JSON for API paths when anonymous", async () => {
|
|
const c = ctx("GET", "/api/me");
|
|
const res = await requireAuth()(c, () => new Response("secret"));
|
|
expect(res.status).toBe(401);
|
|
expect(await res.json()).toEqual({ ok: false, error: "Unauthorized" });
|
|
});
|
|
|
|
test("requireAuth: 302 redirect for page navigations when anonymous", async () => {
|
|
const c = ctx("GET", "/dashboard?tab=1", "text/html");
|
|
const res = await requireAuth()(c, () => new Response("secret"));
|
|
expect(res.status).toBe(302);
|
|
expect(res.headers.get("location")).toBe("/login?next=%2Fdashboard%3Ftab%3D1");
|
|
});
|
|
|
|
test("requireAuth: custom loginPath", async () => {
|
|
const c = ctx("GET", "/dashboard", "text/html");
|
|
const res = await requireAuth({ loginPath: "/signin" })(c, () => new Response("x"));
|
|
expect(res.headers.get("location")).toBe("/signin?next=%2Fdashboard");
|
|
});
|