99 lines
4.0 KiB
TypeScript
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);
|
|
});
|
|
});
|