/** * Webhook Verification Suite * * Pure decision logic for the Stripe webhook policy: event-type allowlist, * payment-status gating, the amount cross-check against the REAL shared price * catalog, plan validation and expiry derivation. Signature verification * itself requires live Stripe secrets (HMAC of the raw body), so it is only * ever exercised end-to-end by the running app — everything that decides * whether a license may be granted is covered here, no database needed. */ import { describe, test, expect } from "./runner"; import { ALLOWED_EVENT_TYPES, isAllowedEventType, isPaymentConfirmed, sessionModeMatchesPlan, subscriptionExpiresAt, verifyPaidAmount, } from "../../src/lib/billing/webhookPolicy"; import { getPlanConfig, isPlanId, resolvePlan } from "../../src/lib/billing/pricing"; describe("WebhookVerification — event type allowlist", () => { test("all handled event types are allowed (including the fixed invoice.payment_succeeded)", () => { expect(isAllowedEventType("checkout.session.completed")).toBe(true); expect(isAllowedEventType("checkout.session.expired")).toBe(true); expect(isAllowedEventType("invoice.payment_failed")).toBe(true); expect(isAllowedEventType("invoice.payment_succeeded")).toBe(true); expect(isAllowedEventType("customer.subscription.updated")).toBe(true); expect(isAllowedEventType("customer.subscription.deleted")).toBe(true); }); test("unhandled and unknown event types are rejected", () => { expect(isAllowedEventType("charge.succeeded")).toBe(false); expect(isAllowedEventType("payment_intent.succeeded")).toBe(false); expect(isAllowedEventType("customer.created")).toBe(false); expect(isAllowedEventType("invoice.created")).toBe(false); expect(isAllowedEventType("")).toBe(false); }); test("the allowlist is not accidentally empty", () => { expect(ALLOWED_EVENT_TYPES.size).toBeGreaterThan(0); // Every allowlisted type is a real Stripe event name pattern, never a wildcard. expect(ALLOWED_EVENT_TYPES.has("*")).toBe(false); }); }); describe("WebhookVerification — payment confirmation gating", () => { test("payment-mode session is confirmed only when payment_status is paid", () => { expect(isPaymentConfirmed({ mode: "payment", payment_status: "paid" }, null)).toBe(true); expect(isPaymentConfirmed({ mode: "payment", payment_status: "unpaid" }, null)).toBe(false); expect(isPaymentConfirmed({ mode: "payment", payment_status: "no_payment_required" }, null)).toBe( false ); expect(isPaymentConfirmed({ mode: "payment", payment_status: null }, null)).toBe(false); }); test("subscription is confirmed while active or trialing", () => { expect(isPaymentConfirmed({ mode: "subscription" }, { status: "active" })).toBe(true); expect(isPaymentConfirmed({ mode: "subscription" }, { status: "trialing" })).toBe(true); }); test("subscription is rejected when canceled, unpaid, past_due or missing", () => { expect(isPaymentConfirmed({ mode: "subscription" }, { status: "canceled" })).toBe(false); expect(isPaymentConfirmed({ mode: "subscription" }, { status: "unpaid" })).toBe(false); expect(isPaymentConfirmed({ mode: "subscription" }, { status: "past_due" })).toBe(false); expect(isPaymentConfirmed({ mode: "subscription" }, { status: "incomplete" })).toBe(false); expect(isPaymentConfirmed({ mode: "subscription" }, null)).toBe(false); expect(isPaymentConfirmed({ mode: "subscription" }, undefined)).toBe(false); }); test("unknown or missing session modes are never confirmed", () => { expect(isPaymentConfirmed({ mode: null, payment_status: "paid" }, null)).toBe(false); expect(isPaymentConfirmed({ mode: "setup", payment_status: "paid" }, null)).toBe(false); expect(isPaymentConfirmed({}, null)).toBe(false); }); }); describe("WebhookVerification — amount cross-check against the real catalog", () => { test("lifetime amount matching the catalog exactly is accepted", () => { const session = { mode: "payment", payment_status: "paid", amount_total: getPlanConfig("lifetime").unitAmountMinor, }; expect(verifyPaidAmount(session, "lifetime")).toBe(true); expect(verifyPaidAmount(session, getPlanConfig("lifetime").id)).toBe(true); // The catalog really is the source: 5999 minor units for lifetime. expect(getPlanConfig("lifetime").unitAmountMinor).toBe(5999); }); test("one cent less than the catalog amount is rejected", () => { expect( verifyPaidAmount( { mode: "payment", payment_status: "paid", amount_total: getPlanConfig("lifetime").unitAmountMinor - 1 }, "lifetime" ) ).toBe(false); }); test("a missing amount_total is rejected for one-time payments", () => { expect( verifyPaidAmount({ mode: "payment", payment_status: "paid", amount_total: null }, "lifetime") ).toBe(false); }); test("subscription sessions are not amount-checked (trial amount may be 0)", () => { expect(verifyPaidAmount({ mode: "subscription", amount_total: 0 }, "weekly")).toBe(true); expect(verifyPaidAmount({ mode: "subscription", amount_total: 0 }, "annual")).toBe(true); expect(verifyPaidAmount({ mode: "subscription", amount_total: 1 }, "weekly")).toBe(true); }); }); describe("WebhookVerification — plan validation", () => { test("known plan ids validate", () => { expect(isPlanId("weekly")).toBe(true); expect(isPlanId("annual")).toBe(true); expect(isPlanId("lifetime")).toBe(true); }); test("unknown and missing plans are rejected by isPlanId", () => { expect(isPlanId("enterprise")).toBe(false); expect(isPlanId("free")).toBe(false); expect(isPlanId("")).toBe(false); expect(isPlanId(null)).toBe(false); expect(isPlanId(undefined)).toBe(false); }); test("resolvePlan falls back to annual for unknown or absent values", () => { expect(resolvePlan("lifetime")).toBe("lifetime"); expect(resolvePlan("weekly")).toBe("weekly"); expect(resolvePlan("annual")).toBe("annual"); expect(resolvePlan("bogus")).toBe("annual"); expect(resolvePlan(null)).toBe("annual"); expect(resolvePlan(undefined)).toBe("annual"); expect(resolvePlan("")).toBe("annual"); }); test("the session mode must match the plan's billing mode", () => { expect(sessionModeMatchesPlan("payment", "lifetime")).toBe(true); expect(sessionModeMatchesPlan("subscription", "weekly")).toBe(true); expect(sessionModeMatchesPlan("subscription", "annual")).toBe(true); expect(sessionModeMatchesPlan("subscription", "lifetime")).toBe(false); expect(sessionModeMatchesPlan("payment", "annual")).toBe(false); }); }); describe("WebhookVerification — expiry derivation from the subscription", () => { test("expiresAt comes from current_period_end, never Date.now()", () => { const periodEnd = 1_700_000_000; // fixed point in the past — must not shift const expires = subscriptionExpiresAt({ status: "trialing", current_period_end: periodEnd }); expect(expires?.getTime()).toBe(periodEnd * 1000); // A fixed period end must yield a fixed expiry regardless of when the test runs. expect(Date.now()).toBeGreaterThan(periodEnd * 1000); }); test("missing period end or missing subscription yields no expiry", () => { expect(subscriptionExpiresAt({ status: "active", current_period_end: null })).toBeNull(); expect(subscriptionExpiresAt({ status: "active" })).toBeNull(); expect(subscriptionExpiresAt(null)).toBeNull(); expect(subscriptionExpiresAt(undefined)).toBeNull(); }); }); describe("WebhookVerification — catalog contract used by the webhook", () => { test("lifetime is a one-time payment; weekly and annual are subscriptions", () => { expect(getPlanConfig("lifetime").mode).toBe("payment"); expect(getPlanConfig("weekly").mode).toBe("subscription"); expect(getPlanConfig("annual").mode).toBe("subscription"); }); test("catalog amounts match the pricing guide (EUR minor units)", () => { expect(getPlanConfig("lifetime").unitAmountMinor).toBe(5999); expect(getPlanConfig("weekly").unitAmountMinor).toBe(499); expect(getPlanConfig("annual").unitAmountMinor).toBe(3999); }); });