Files
2026-07-12 15:55:18 +05:30

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");
});