Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/email/unsubscribeToken.test.ts
T

99 lines
4.0 KiB
TypeScript

import { describe, it, expect, afterEach } from "vitest";
import {
buildUnsubscribeUrl,
signUnsubscribeToken,
verifyUnsubscribeToken,
UnsubscribeTokenSecretMissingError,
} from "./unsubscribeToken.server";
const SECRET = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
afterEach(() => {
delete process.env.MARKETING_TOKEN_SECRET;
delete process.env.LEAD_CONVERSION_SECRET;
});
describe("signUnsubscribeToken", () => {
it("throws when no secret is configured", () => {
expect(() => signUnsubscribeToken({ runId: "run_1" })).toThrow(UnsubscribeTokenSecretMissingError);
});
it("throws when secret is too short", () => {
expect(() => signUnsubscribeToken({ runId: "run_1", secret: "short" })).toThrow(UnsubscribeTokenSecretMissingError);
});
it("requires runId", () => {
expect(() => signUnsubscribeToken({ runId: "" as unknown as string, secret: SECRET })).toThrow(TypeError);
});
it("returns a u1.<payload>.<sig> string", () => {
const t = signUnsubscribeToken({ runId: "run_1", secret: SECRET });
expect(t.split(".").length).toBe(3);
expect(t.startsWith("u1.")).toBe(true);
});
it("uses MARKETING_TOKEN_SECRET if set", () => {
process.env.MARKETING_TOKEN_SECRET = SECRET;
const t = signUnsubscribeToken({ runId: "run_1" });
expect(verifyUnsubscribeToken(t)).toMatchObject({ runId: "run_1" });
});
it("falls back to LEAD_CONVERSION_SECRET", () => {
process.env.LEAD_CONVERSION_SECRET = SECRET;
const t = signUnsubscribeToken({ runId: "run_1" });
expect(verifyUnsubscribeToken(t)).toMatchObject({ runId: "run_1" });
});
});
describe("verifyUnsubscribeToken", () => {
it("returns null for null/empty input", () => {
expect(verifyUnsubscribeToken(null, { secret: SECRET })).toBeNull();
expect(verifyUnsubscribeToken("", { secret: SECRET })).toBeNull();
});
it("returns null when secret missing", () => {
const t = signUnsubscribeToken({ runId: "run_1", secret: SECRET });
expect(verifyUnsubscribeToken(t)).toBeNull();
});
it("returns null on tampered signature", () => {
const t = signUnsubscribeToken({ runId: "run_1", secret: SECRET });
const tampered = `${t.slice(0, -2)}XX`;
expect(verifyUnsubscribeToken(tampered, { secret: SECRET })).toBeNull();
});
it("returns null on tampered payload", () => {
const t = signUnsubscribeToken({ runId: "run_1", secret: SECRET });
const parts = t.split(".");
const tamperedPayload = Buffer.from(JSON.stringify({ runId: "run_2", iat: 0 }), "utf8").toString("base64url");
const tampered = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
expect(verifyUnsubscribeToken(tampered, { secret: SECRET })).toBeNull();
});
it("returns null on wrong version prefix", () => {
const t = signUnsubscribeToken({ runId: "run_1", secret: SECRET });
const tampered = t.replace("u1.", "u2.");
expect(verifyUnsubscribeToken(tampered, { secret: SECRET })).toBeNull();
});
it("returns the payload on valid token", () => {
const t = signUnsubscribeToken({ runId: "run_1", leadId: "lead_1", secret: SECRET });
const p = verifyUnsubscribeToken(t, { secret: SECRET });
expect(p).toMatchObject({ runId: "run_1", leadId: "lead_1", tenantId: null });
});
it("returns null when verifying with a different secret", () => {
const t = signUnsubscribeToken({ runId: "run_1", secret: SECRET });
const other = "f".repeat(64);
expect(verifyUnsubscribeToken(t, { secret: other })).toBeNull();
});
});
describe("buildUnsubscribeUrl", () => {
it("builds an absolute https url with the token", () => {
const url = buildUnsubscribeUrl({
baseUrl: "https://app.example.com",
runId: "run_1",
secret: SECRET,
});
expect(url.startsWith("https://app.example.com/unsubscribe/")).toBe(true);
});
it("strips trailing slashes from the base url", () => {
const url = buildUnsubscribeUrl({
baseUrl: "https://app.example.com/",
runId: "run_1",
secret: SECRET,
});
expect(url.startsWith("https://app.example.com/unsubscribe/")).toBe(true);
});
});