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