import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { LeadTokenSecretMissingError, signLeadToken, verifyLeadToken } from "./leadToken.server"; const TEST_SECRET = "x".repeat(48); describe("leadToken", () => { const originalEnv = process.env.LEAD_CONVERSION_SECRET; beforeEach(() => { process.env.LEAD_CONVERSION_SECRET = TEST_SECRET; }); afterEach(() => { if (originalEnv === undefined) delete process.env.LEAD_CONVERSION_SECRET; else process.env.LEAD_CONVERSION_SECRET = originalEnv; }); it("sign + verify roundtrip returns same leadId and planId", () => { const tok = signLeadToken({ leadId: "lead_1", planId: "plan_pro" }); const decoded = verifyLeadToken(tok); expect(decoded).toMatchObject({ leadId: "lead_1", planId: "plan_pro" }); expect(decoded?.exp).toBeGreaterThan(decoded?.iat ?? 0); }); it("defaults to 7-day TTL", () => { const fixedNow = () => new Date("2026-05-17T00:00:00Z"); const tok = signLeadToken({ leadId: "lead_1", now: fixedNow }); const decoded = verifyLeadToken(tok, { now: fixedNow }); expect(decoded).not.toBeNull(); expect((decoded!.exp - decoded!.iat) / (24 * 60 * 60)).toBeCloseTo(7); }); it("clamps TTL to max 30 days", () => { const fixedNow = () => new Date("2026-05-17T00:00:00Z"); const tok = signLeadToken({ leadId: "lead_1", expiresInDays: 9999, now: fixedNow }); const decoded = verifyLeadToken(tok, { now: fixedNow }); expect(decoded).not.toBeNull(); expect((decoded!.exp - decoded!.iat) / (24 * 60 * 60)).toBeLessThanOrEqual(30); }); it("returns null for expired tokens", () => { const past = () => new Date("2026-01-01T00:00:00Z"); const future = () => new Date("2026-12-31T00:00:00Z"); const tok = signLeadToken({ leadId: "lead_1", expiresInDays: 1, now: past }); expect(verifyLeadToken(tok, { now: future })).toBeNull(); }); it("returns null for tampered payload", () => { const tok = signLeadToken({ leadId: "lead_1" }); const [v, p, s] = tok.split("."); // Re-encode with a different leadId — signature should no longer match. const tampered = Buffer.from(JSON.stringify({ leadId: "lead_hijack", iat: 0, exp: 2 ** 31 - 1 }), "utf8").toString("base64url"); expect(verifyLeadToken(`${v}.${tampered}.${s}`)).toBeNull(); expect(p).toBeDefined(); }); it("returns null for tampered signature", () => { const tok = signLeadToken({ leadId: "lead_1" }); const [v, p] = tok.split("."); const badSig = Buffer.alloc(32, 0).toString("base64url"); expect(verifyLeadToken(`${v}.${p}.${badSig}`)).toBeNull(); }); it("returns null when version prefix is wrong", () => { const tok = signLeadToken({ leadId: "lead_1" }); const [, p, s] = tok.split("."); expect(verifyLeadToken(`v2.${p}.${s}`)).toBeNull(); }); it("returns null for malformed tokens", () => { expect(verifyLeadToken("")).toBeNull(); expect(verifyLeadToken(null)).toBeNull(); expect(verifyLeadToken(undefined)).toBeNull(); expect(verifyLeadToken("not.a.token.extrapart")).toBeNull(); expect(verifyLeadToken("v1.@@@.bad")).toBeNull(); }); it("throws when LEAD_CONVERSION_SECRET is unset", () => { delete process.env.LEAD_CONVERSION_SECRET; expect(() => signLeadToken({ leadId: "lead_1" })).toThrow(LeadTokenSecretMissingError); }); it("throws when LEAD_CONVERSION_SECRET is too short", () => { process.env.LEAD_CONVERSION_SECRET = "too-short"; expect(() => signLeadToken({ leadId: "lead_1" })).toThrow(LeadTokenSecretMissingError); }); it("verify returns null when secret env is missing", () => { const tok = signLeadToken({ leadId: "lead_1", secret: TEST_SECRET }); delete process.env.LEAD_CONVERSION_SECRET; expect(verifyLeadToken(tok)).toBeNull(); }); it("rejects empty leadId on sign", () => { expect(() => signLeadToken({ leadId: "" })).toThrow(TypeError); }); it("rotation: token signed with secret-A fails verify under secret-B", () => { const secretA = "a".repeat(48); const secretB = "b".repeat(48); const tok = signLeadToken({ leadId: "lead_1", secret: secretA }); expect(verifyLeadToken(tok, { secret: secretB })).toBeNull(); expect(verifyLeadToken(tok, { secret: secretA })?.leadId).toBe("lead_1"); }); });