Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/leads/leadToken.test.ts
T

106 lines
4.2 KiB
TypeScript

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