66 lines
2.6 KiB
TypeScript
66 lines
2.6 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
import { createContext } from "@wrnexus/core";
|
|
import { signJwt, verifyJwt, jwtAuth, JwtError } from "../src/index.ts";
|
|
|
|
const SECRET = "test-secret-key";
|
|
|
|
test("sign + verify round-trip preserves claims", async () => {
|
|
const token = await signJwt({ sub: "u1", role: "admin" }, SECRET);
|
|
const claims = await verifyJwt(token, SECRET);
|
|
expect(claims.sub).toBe("u1");
|
|
expect(claims.role).toBe("admin");
|
|
expect(typeof claims.iat).toBe("number");
|
|
});
|
|
|
|
test("tampering or wrong secret fails verification", async () => {
|
|
const token = await signJwt({ sub: "u1" }, SECRET);
|
|
await expect(verifyJwt(token, "other-secret")).rejects.toThrow(JwtError);
|
|
const tampered = token.slice(0, -2) + (token.endsWith("a") ? "bb" : "aa");
|
|
await expect(verifyJwt(tampered, SECRET)).rejects.toThrow();
|
|
});
|
|
|
|
test("rejects a signed token whose header declares another algorithm", async () => {
|
|
const token = await signJwt({ sub: "u1" }, SECRET);
|
|
const [, body] = token.split(".");
|
|
const header = btoa(JSON.stringify({ alg: "none", typ: "JWT" }))
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_")
|
|
.replace(/=+$/, "");
|
|
await expect(verifyJwt(`${header}.${body}.invalid`, SECRET)).rejects.toThrow(
|
|
"Unsupported token header",
|
|
);
|
|
});
|
|
|
|
test("expiry is enforced", async () => {
|
|
const token = await signJwt({ sub: "u1" }, SECRET, { expiresIn: 100, now: 1000 });
|
|
expect((await verifyJwt(token, SECRET, { now: 1050 })).sub).toBe("u1"); // still valid
|
|
await expect(verifyJwt(token, SECRET, { now: 1200 })).rejects.toThrow("expired");
|
|
});
|
|
|
|
function ctxWith(auth?: string) {
|
|
const url = new URL("http://x/api/me");
|
|
return createContext(new Request(url, { headers: auth ? { authorization: auth } : {} }), url);
|
|
}
|
|
|
|
test("jwtAuth sets ctx.user for a valid bearer token", async () => {
|
|
const token = await signJwt({ sub: "u9" }, SECRET);
|
|
const ctx = ctxWith(`Bearer ${token}`);
|
|
const res = await jwtAuth({ secret: SECRET })(ctx, () => new Response("ok"));
|
|
expect(res.status).toBe(200);
|
|
expect((ctx.user as { sub: string }).sub).toBe("u9");
|
|
});
|
|
|
|
test("jwtAuth 401s a missing/invalid token when required", async () => {
|
|
const missing = await jwtAuth({ secret: SECRET })(ctxWith(), () => new Response("ok"));
|
|
expect(missing.status).toBe(401);
|
|
const bad = await jwtAuth({ secret: SECRET })(ctxWith("Bearer nope"), () => new Response("ok"));
|
|
expect(bad.status).toBe(401);
|
|
});
|
|
|
|
test("jwtAuth optional mode passes through anonymously", async () => {
|
|
const ctx = ctxWith();
|
|
const res = await jwtAuth({ secret: SECRET, required: false })(ctx, () => new Response("ok"));
|
|
expect(res.status).toBe(200);
|
|
expect(ctx.user).toBeUndefined();
|
|
});
|