diff --git a/tests/int/session-token.int.spec.ts b/tests/int/session-token.int.spec.ts new file mode 100644 index 0000000..9aab66b --- /dev/null +++ b/tests/int/session-token.int.spec.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { decodeTokenExp, getSessionExpMs } from "@/lib/session/token"; + +/** Build a JWT-shaped string with the given payload object. */ +function makeToken(payload: Record): string { + const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const body = btoa(JSON.stringify(payload)); + return `${header}.${body}.signature`; +} + +describe("decodeTokenExp", () => { + it("returns the numeric exp claim from a well-formed token", () => { + const exp = 1_800_000_000; + expect(decodeTokenExp(makeToken({ sub: "1", exp }))).toBe(exp); + }); + + it("returns null when the token has no payload segment", () => { + expect(decodeTokenExp("header")).toBeNull(); + expect(decodeTokenExp("")).toBeNull(); + }); + + it("returns null when the payload is not valid base64 JSON", () => { + expect(decodeTokenExp("header.not-json.signature")).toBeNull(); + }); + + it("returns null when exp is missing or not a finite number", () => { + expect(decodeTokenExp(makeToken({ sub: "1" }))).toBeNull(); + expect(decodeTokenExp(makeToken({ exp: "soon" }))).toBeNull(); + expect(decodeTokenExp(makeToken({ exp: null }))).toBeNull(); + expect(decodeTokenExp(makeToken({ exp: Number.NaN }))).toBeNull(); + }); + + it("handles URL-safe base64 (JWT padding) in the payload", () => { + // A payload whose base64 would contain - and _ after URL-safe encoding. + const payload = { exp: 1_800_000_000, data: "a+b/c=d" }; + const body = btoa(JSON.stringify(payload)).replace(/\+/g, "-").replace(/\//g, "_"); + const token = `header.${body}.signature`; + expect(decodeTokenExp(token)).toBe(1_800_000_000); + }); +}); + +describe("getSessionExpMs", () => { + const exp = 1_800_000_000; + const token = makeToken({ sub: "1", exp }); + + it("returns the exp in milliseconds from the cookie header", () => { + const headers = { get: (name: string) => (name === "cookie" ? `payload-token=${token}` : null) }; + expect(getSessionExpMs(headers)).toBe(exp * 1000); + }); + + it("returns null when there is no cookie header", () => { + const headers = { get: () => null }; + expect(getSessionExpMs(headers)).toBeNull(); + }); + + it("returns null when the payload-token cookie is absent", () => { + const headers = { get: () => "other=value" }; + expect(getSessionExpMs(headers)).toBeNull(); + }); + + it("parses the token among other cookies", () => { + const headers = { get: () => `foo=1; payload-token=${token}; bar=2` }; + expect(getSessionExpMs(headers)).toBe(exp * 1000); + }); + + it("returns null when the token is malformed", () => { + const headers = { get: () => "payload-token=not-a-jwt" }; + expect(getSessionExpMs(headers)).toBeNull(); + }); +});