← back to Charge And Explore
backend/test/tiers.test.ts
181 lines
// Tier → entitlement rules (billing, TK-10227). Proves the FREE/PLUS/PRO
// gating math, the degrade-to-free normalization, the webhook→tier mapping,
// and the StripeBilling hard rails (test-mode-only key, inert by default).
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
TIERS, TIER_ORDER, PAID_TIERS,
normalizeTier, isPaidTier, entitlementsFor, tierAtLeast,
clampHistoryHours, clampHistoryDays, vehicleLimit, tierFromStripeObject,
} from "../src/core/tiers.ts";
import { StripeBilling } from "../src/providers/stripe-billing.ts";
describe("tier model", () => {
it("defines exactly free/plus/pro in ascending order", () => {
assert.deepEqual([...TIER_ORDER], ["free", "plus", "pro"]);
assert.deepEqual([...PAID_TIERS], ["plus", "pro"]);
assert.deepEqual(Object.keys(TIERS).sort(), ["free", "plus", "pro"]);
});
it("free = 7-day history + 1 vehicle, no parked analytics", () => {
const e = entitlementsFor("free");
assert.equal(e.historyDays, 7);
assert.equal(e.maxVehicles, 1);
assert.equal(e.parkedAnalytics, false);
assert.equal(e.prioritySampling, false);
assert.equal(e.priceCentsMonthly, 0);
});
it("plus = 90-day history + all vehicles ($4.99 placeholder)", () => {
const e = entitlementsFor("plus");
assert.equal(e.historyDays, 90);
assert.equal(e.maxVehicles, null);
assert.equal(e.parkedAnalytics, false);
assert.equal(e.priceCentsMonthly, 499);
});
it("pro = full history + parked analytics + priority sampling ($9.99 placeholder)", () => {
const e = entitlementsFor("pro");
assert.equal(e.historyDays, null);
assert.equal(e.maxVehicles, null);
assert.equal(e.parkedAnalytics, true);
assert.equal(e.prioritySampling, true);
assert.equal(e.priceCentsMonthly, 999);
});
it("normalizeTier degrades anything unknown to free (never throws)", () => {
assert.equal(normalizeTier("plus"), "plus");
assert.equal(normalizeTier("pro"), "pro");
assert.equal(normalizeTier("free"), "free");
assert.equal(normalizeTier("enterprise"), "free");
assert.equal(normalizeTier(undefined), "free");
assert.equal(normalizeTier(null), "free");
assert.equal(normalizeTier(42), "free");
});
it("isPaidTier accepts only plus/pro", () => {
assert.equal(isPaidTier("plus"), true);
assert.equal(isPaidTier("pro"), true);
assert.equal(isPaidTier("free"), false);
assert.equal(isPaidTier("PLUS"), false);
assert.equal(isPaidTier(""), false);
});
it("tierAtLeast follows the free < plus < pro order", () => {
assert.equal(tierAtLeast("pro", "plus"), true);
assert.equal(tierAtLeast("plus", "plus"), true);
assert.equal(tierAtLeast("free", "plus"), false);
assert.equal(tierAtLeast("plus", "pro"), false);
});
});
describe("history clamping", () => {
it("free clamps to 7 days (168h) — smaller requests pass through", () => {
assert.equal(clampHistoryHours("free", 720), 168);
assert.equal(clampHistoryHours("free", 168), 168);
assert.equal(clampHistoryHours("free", 24), 24);
assert.equal(clampHistoryDays("free", 30), 7);
assert.equal(clampHistoryDays("free", 3), 3);
});
it("plus clamps to 90 days (2160h)", () => {
assert.equal(clampHistoryHours("plus", 2160), 2160);
assert.equal(clampHistoryHours("plus", 3000), 2160);
assert.equal(clampHistoryDays("plus", 90), 90);
assert.equal(clampHistoryDays("plus", 365), 90);
});
it("pro is unlimited — every request passes through", () => {
assert.equal(clampHistoryHours("pro", 999999), 999999);
assert.equal(clampHistoryDays("pro", 3650), 3650);
});
it("vehicleLimit: free=1, plus/pro=all", () => {
assert.equal(vehicleLimit("free"), 1);
assert.equal(vehicleLimit("plus"), Infinity);
assert.equal(vehicleLimit("pro"), Infinity);
});
});
describe("tierFromStripeObject (webhook mapping, pure)", () => {
it("active subscription with ce_tier metadata grants that tier", () => {
assert.equal(tierFromStripeObject({ status: "active", metadata: { ce_tier: "pro" } }), "pro");
assert.equal(tierFromStripeObject({ status: "trialing", metadata: { ce_tier: "plus" } }), "plus");
});
it("falls back to the price's metadata when the subscription has none", () => {
const obj = { status: "active", items: { data: [{ price: { metadata: { ce_tier: "plus" } } }] } };
assert.equal(tierFromStripeObject(obj), "plus");
});
it("non-active statuses revoke to free", () => {
for (const status of ["past_due", "canceled", "unpaid", "incomplete_expired"]) {
assert.equal(tierFromStripeObject({ status, metadata: { ce_tier: "pro" } }), "free");
}
});
it("missing/garbage objects degrade to free", () => {
assert.equal(tierFromStripeObject(null), "free");
assert.equal(tierFromStripeObject(undefined), "free");
assert.equal(tierFromStripeObject({}), "free");
assert.equal(tierFromStripeObject({ status: "active", metadata: { ce_tier: "gold" } }), "free");
});
});
describe("StripeBilling hard rails (no network)", () => {
it("is INERT by default — BILLING_ENABLED unset means configured:false even with a key", () => {
const b = new StripeBilling({ STRIPE_TEST_SECRET_KEY: "sk_test_abc" });
assert.equal(b.enabled, false);
assert.equal(b.configured, false);
});
it("BILLING_ENABLED=1 + sk_test_ key = configured", () => {
const b = new StripeBilling({ BILLING_ENABLED: "1", STRIPE_TEST_SECRET_KEY: "sk_test_abc" });
assert.equal(b.configured, true);
});
it("REFUSES a live-mode key in TEST mode — sk_live_ never configures test billing", () => {
const b = new StripeBilling({ BILLING_ENABLED: "1", STRIPE_TEST_SECRET_KEY: "sk_live_abc" });
assert.equal(b.live, false);
assert.equal(b.keyOk, false);
assert.equal(b.configured, false);
});
it("BILLING_LIVE=1 requires a real sk_live_ key — the flag alone (with a test key) stays unconfigured", () => {
// Live mode reads STRIPE_LIVE_SECRET_KEY, so a test key in the test var is invisible here.
const b = new StripeBilling({ BILLING_ENABLED: "1", BILLING_LIVE: "1", STRIPE_TEST_SECRET_KEY: "sk_test_abc" });
assert.equal(b.live, true);
assert.equal(b.configured, false);
});
it("BILLING_LIVE=1 + sk_live_ key = configured (the deliberate two-part live gate)", () => {
const b = new StripeBilling({ BILLING_ENABLED: "1", BILLING_LIVE: "1", STRIPE_LIVE_SECRET_KEY: "sk_live_abc" });
assert.equal(b.live, true);
assert.equal(b.keyOk, true);
assert.equal(b.configured, true);
});
it("live/test env vars never cross modes — an sk_test_ key can't leak into live", () => {
const b = new StripeBilling({ BILLING_ENABLED: "1", BILLING_LIVE: "1", STRIPE_LIVE_SECRET_KEY: "sk_test_abc" });
assert.equal(b.keyOk, false);
assert.equal(b.configured, false);
});
it("no key / garbage key = unconfigured", () => {
assert.equal(new StripeBilling({ BILLING_ENABLED: "1" }).configured, false);
assert.equal(new StripeBilling({ BILLING_ENABLED: "1", STRIPE_TEST_SECRET_KEY: "pk_test_abc" }).configured, false);
});
it("webhook verification refuses to run without a webhook secret", async () => {
const b = new StripeBilling({ BILLING_ENABLED: "1", STRIPE_TEST_SECRET_KEY: "sk_test_abc" });
assert.equal(b.hasWebhookSecret, false);
await assert.rejects(() => b.verifyWebhook("{}", "t=1,v1=x"), /STRIPE_TEST_WEBHOOK_SECRET not set/);
});
it("unconfigured billing throws before ever touching the Stripe SDK", async () => {
const b = new StripeBilling({});
await assert.rejects(() => b.ensurePriceId("plus"), /billing not configured/);
await assert.rejects(() => b.createCheckoutSession({ tier: "pro", userSub: "s", email: "e@x.com", baseUrl: "https://x" }), /billing not configured/);
});
});