import { describe, expect, it } from "vitest"; import { decodeToken, parseTtl, signToken, TokenError, verifyToken } from "../jwt.js"; const SECRET = "unit-test-secret-0123456789"; describe("jwt (hand-rolled HS256)", () => { it("signs and verifies a roundtrip with claims intact", () => { const token = signToken({ sub: "usr_123", workspaces: ["wks_1", "wks_2"], jti: "at_1" }, "15m", SECRET); expect(token.split(".").length).toBe(3); const claims = verifyToken(token, SECRET); expect(claims.sub).toBe("usr_123"); expect(claims.workspaces).toEqual(["wks_1", "wks_2"]); expect(claims.jti).toBe("at_1"); expect(typeof claims.iat).toBe("number"); expect(typeof claims.exp).toBe("number"); expect(claims.exp).toBe(claims.iat! + 15 * 60); }); it("rejects a tampered payload", () => { const token = signToken({ sub: "usr_1" }, "15m", SECRET); const [h, b, s] = token.split("."); const forged = Buffer.from(JSON.stringify({ sub: "usr_evil", iat: 1, exp: 9999999999 })).toString("base64url"); expect(() => verifyToken(`${h}.${forged}.${s}`, SECRET)).toThrowError(TokenError); try { verifyToken(`${h}.${forged}.${s}`, SECRET); } catch (err) { expect((err as TokenError).code).toBe("INVALID_TOKEN"); } }); it("rejects a tampered signature", () => { const token = signToken({ sub: "usr_1" }, "15m", SECRET); const [h, b, s] = token.split("."); const flipped = (s[0] === "A" ? "B" : "A") + s.slice(1); expect(() => verifyToken(`${h}.${b}.${flipped}`, SECRET)).toThrowError(TokenError); }); it("rejects a token signed with a different key", () => { const token = signToken({ sub: "usr_1" }, "15m", SECRET); expect(() => verifyToken(token, "other-secret-0123456789abc")).toThrowError(TokenError); }); it("reports TOKEN_EXPIRED for expired tokens", () => { const now = Math.floor(Date.now() / 1000); const token = signToken({ sub: "usr_1", iat: now - 3600, exp: now - 60 }, 60, SECRET); try { verifyToken(token, SECRET); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(TokenError); expect((err as TokenError).code).toBe("TOKEN_EXPIRED"); } }); it("rejects malformed tokens", () => { expect(() => verifyToken("garbage", SECRET)).toThrowError(TokenError); expect(() => verifyToken("a.b", SECRET)).toThrowError(TokenError); expect(() => verifyToken("", SECRET)).toThrowError(TokenError); }); it("rejects non-HS256 algorithm headers (alg confusion guard)", () => { const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url"); const body = Buffer.from(JSON.stringify({ sub: "usr_1", iat: 1, exp: 9999999999 })).toString("base64url"); expect(() => verifyToken(`${header}.${body}.`, SECRET)).toThrowError(TokenError); }); it("decodeToken returns claims without verifying (and null on garbage)", () => { const token = signToken({ sub: "usr_9" }, "15m", SECRET); expect(decodeToken(token)?.sub).toBe("usr_9"); expect(decodeToken("nope")).toBeNull(); }); it("parseTtl handles all units", () => { expect(parseTtl("15m")).toBe(900); expect(parseTtl("30d")).toBe(30 * 86400); expect(parseTtl("2h")).toBe(7200); expect(parseTtl("90s")).toBe(90); expect(parseTtl("2000ms")).toBe(2); expect(() => parseTtl("soon")).toThrow(); }); });