import { beforeEach, describe, expect, it } from 'vitest' import { validateReport } from '@extant2000/evidence-record' import { DEFAULT_QUOTAS, explainPlan, formatAccess, toReport, MemoryStore, createLimiter, effectivePlan, hasEntitlement, meetsPlan, planRank, createSyncLimiter, planWriteQuota, type SuiteGating, } from '../src/index.js' const gating: SuiteGating = { plans: ['free', 'pro', 'govcon'], planEntitlements: { free: ['dashboard'], pro: ['dashboard', 'exports'], govcon: ['dashboard', 'exports', 'audit'], }, adminRole: 'admin', roleField: 'role', } describe('effectivePlan', () => { it('returns empty string when signed out', () => { expect(effectivePlan(null, gating)).toBe('') expect(effectivePlan({ profile: null }, gating)).toBe('') }) it('falls back to the base plan with no entitlement row', () => { expect(effectivePlan({ profile: {} }, gating)).toBe('free') }) it('grants the paid plan on an active subscription', () => { expect(effectivePlan( { profile: {}, entitlement: { plan: 'pro', status: 'active' } }, gating, )).toBe('pro') }) it('keeps access while past_due — dunning decides, not the gate', () => { expect(effectivePlan( { profile: {}, entitlement: { plan: 'pro', status: 'past_due' } }, gating, )).toBe('pro') }) it('drops to base on a cancelled subscription', () => { expect(effectivePlan( { profile: {}, entitlement: { plan: 'pro', status: 'canceled' } }, gating, )).toBe('free') }) it('grants the top plan to the admin role', () => { expect(effectivePlan({ profile: { role: 'admin' } }, gating)).toBe('govcon') }) it('honours a custom roleField', () => { const g = { ...gating, roleField: 'hr_role' } expect(effectivePlan({ profile: { hr_role: 'admin' } }, g)).toBe('govcon') // The default 'role' field must NOT be consulted once roleField is set. expect(effectivePlan({ profile: { role: 'admin' } }, g)).toBe('free') }) it('CROSS-PRODUCT ISOLATION: a sibling product subscription does not unlock this one', () => { // Several products share one identity store. A billing field on the shared // profile must never grant scope — only the product-scoped entitlement row. const src = { profile: { stripe_subscription_status: 'active', stripe_plan: 'govcon' }, entitlement: null, } expect(effectivePlan(src, gating)).toBe('free') }) it('ignores a plan key that is not in this product gating', () => { expect(effectivePlan( { profile: {}, entitlement: { plan: 'enterprise', status: 'active' } }, gating, )).toBe('free') }) }) describe('hasEntitlement / planRank / meetsPlan', () => { it('gates features by the effective plan', () => { const pro = { profile: {}, entitlement: { plan: 'pro', status: 'active' } } expect(hasEntitlement(pro, 'exports', gating)).toBe(true) expect(hasEntitlement(pro, 'audit', gating)).toBe(false) }) it('ranks plans by declared order', () => { expect(planRank('free', gating)).toBe(0) expect(planRank('govcon', gating)).toBe(2) expect(planRank('nope', gating)).toBe(-1) }) it('meetsPlan compares rank', () => { const pro = { profile: {}, entitlement: { plan: 'pro', status: 'active' } } expect(meetsPlan(pro, 'free', gating)).toBe(true) expect(meetsPlan(pro, 'pro', gating)).toBe(true) expect(meetsPlan(pro, 'govcon', gating)).toBe(false) }) it('treats an UNKNOWN minPlan as not-gated (documented fail-open)', () => { // Deliberate: this model drives surfaces shown locked rather than removed, // so a typo costs a visible feature, not a silent one. expect(meetsPlan({ profile: {} }, 'typoo', gating)).toBe(true) }) }) describe('planWriteQuota', () => { it('uses the default table', () => { expect(planWriteQuota('free')).toBe(DEFAULT_QUOTAS.free) expect(planWriteQuota('pro')).toBe(DEFAULT_QUOTAS.pro) }) it('falls back to default for an unknown plan', () => { expect(planWriteQuota('mystery')).toBe(DEFAULT_QUOTAS.default) }) it('treats a null plan as none', () => { expect(planWriteQuota(null)).toBe(DEFAULT_QUOTAS.none) }) it('returns 0 (unlimited) for enterprise', () => { expect(planWriteQuota('enterprise')).toBe(0) }) it('lets a global env var override', () => { expect(planWriteQuota('free', { env: { RATE_LIMIT_WRITES_FREE: '5' } })).toBe(5) }) it('lets a per-product env var beat the global', () => { const env = { RATE_LIMIT_WRITES_FREE: '5', RATE_LIMIT_WRITES_CRM_FREE: '99' } expect(planWriteQuota('free', { env, product: 'crm' })).toBe(99) }) it('accepts the literal "unlimited"', () => { expect(planWriteQuota('free', { env: { RATE_LIMIT_WRITES_FREE: 'unlimited' } })).toBe(0) }) it('ignores a malformed env value rather than failing open to 0', () => { // A negative or non-numeric override must not silently become "unlimited". expect(planWriteQuota('free', { env: { RATE_LIMIT_WRITES_FREE: 'abc' } })).toBe(60) expect(planWriteQuota('free', { env: { RATE_LIMIT_WRITES_FREE: '-1' } })).toBe(60) }) }) describe('createLimiter', () => { let store: MemoryStore beforeEach(() => { store = new MemoryStore() }) it('allows up to the quota then rejects', async () => { const check = createLimiter({ store, env: { RATE_LIMIT_WRITES_FREE: '3' } }) const input = { product: 'crm', userId: 'u1', plan: 'free' } for (let i = 0; i < 3; i++) { expect((await check(input)).allowed).toBe(true) } const denied = await check(input) expect(denied.allowed).toBe(false) expect(denied.limit).toBe(3) expect(denied.remaining).toBe(0) expect(denied.retryAfter).toBeGreaterThan(0) expect(denied.message).toContain('3 changes/minute') }) it('counts down remaining', async () => { const check = createLimiter({ store, env: { RATE_LIMIT_WRITES_FREE: '3' } }) const input = { product: 'crm', userId: 'u1', plan: 'free' } expect((await check(input)).remaining).toBe(2) expect((await check(input)).remaining).toBe(1) expect((await check(input)).remaining).toBe(0) }) it('budgets each account separately', async () => { const check = createLimiter({ store, env: { RATE_LIMIT_WRITES_FREE: '1' } }) expect((await check({ product: 'crm', userId: 'u1', plan: 'free' })).allowed).toBe(true) expect((await check({ product: 'crm', userId: 'u2', plan: 'free' })).allowed).toBe(true) expect((await check({ product: 'crm', userId: 'u1', plan: 'free' })).allowed).toBe(false) }) it('budgets each product separately for the same account', async () => { const check = createLimiter({ store, env: { RATE_LIMIT_WRITES_FREE: '1' } }) expect((await check({ product: 'crm', userId: 'u1', plan: 'free' })).allowed).toBe(true) expect((await check({ product: 'books', userId: 'u1', plan: 'free' })).allowed).toBe(true) }) it('never limits an unlimited plan', async () => { const check = createLimiter({ store }) for (let i = 0; i < 50; i++) { expect((await check({ product: 'crm', userId: 'u1', plan: 'enterprise' })).allowed).toBe(true) } }) it('does not bill unauthenticated writes to a shared empty key', async () => { // Otherwise one anonymous caller exhausts the budget for every other one. const check = createLimiter({ store, env: { RATE_LIMIT_WRITES_FREE: '1' } }) for (let i = 0; i < 10; i++) { expect((await check({ product: 'crm', userId: '', plan: 'free' })).allowed).toBe(true) } }) it('accepts an injected shared store — the multi-instance fix', async () => { // Two "instances" sharing one store must share one budget. With the old // module-level Map, each instance kept its own counters and the effective // limit silently became N× the configured one. const shared = new MemoryStore() const a = createLimiter({ store: shared, env: { RATE_LIMIT_WRITES_FREE: '2' } }) const b = createLimiter({ store: shared, env: { RATE_LIMIT_WRITES_FREE: '2' } }) const input = { product: 'crm', userId: 'u1', plan: 'free' } expect((await a(input)).allowed).toBe(true) expect((await b(input)).allowed).toBe(true) expect((await a(input)).allowed).toBe(false) }) }) describe('createSyncLimiter', () => { it('returns a decision synchronously, not a promise', () => { // The reason this exists: every product middleware calls the limiter // WITHOUT await. If the limiter were async, the 429 would surface as an // unhandled rejection and the write would proceed — a limit that reports // correctly and enforces nothing. const check = createSyncLimiter({ store: new MemoryStore(), env: { RATE_LIMIT_WRITES_FREE: '2' }, }) const d = check({ product: 'crm', userId: 'u1', plan: 'free' }) expect(d).not.toBeInstanceOf(Promise) expect(d.allowed).toBe(true) }) it('enforces the same budget as the async limiter', () => { const check = createSyncLimiter({ store: new MemoryStore(), env: { RATE_LIMIT_WRITES_FREE: '2' }, }) const input = { product: 'crm', userId: 'u1', plan: 'free' } expect(check(input).allowed).toBe(true) expect(check(input).allowed).toBe(true) expect(check(input).allowed).toBe(false) }) it('exempts unlimited plans and anonymous callers', () => { const check = createSyncLimiter({ store: new MemoryStore() }) expect(check({ product: 'crm', userId: 'u1', plan: 'enterprise' }).limit).toBe(Infinity) expect(check({ product: 'crm', userId: '', plan: 'free' }).limit).toBe(Infinity) }) }) describe('explainPlan — a decision you can answer for', () => { const gating: SuiteGating = { plans: ['free', 'pro', 'enterprise'], adminRole: 'admin', } const pro = { profile: { id: 'u1' }, entitlement: { plan: 'pro', status: 'active' } } it('separates a genuine grant from a fail-open on an unknown plan', () => { // meetsPlan returns true for both. Only one of them is a grant. expect(meetsPlan(pro, 'pro', gating)).toBe(true) expect(meetsPlan(pro, 'tpyo', gating)).toBe(true) expect(explainPlan(pro, 'pro', gating).determination).toBe('pass') const typo = explainPlan(pro, 'tpyo', gating) expect(typo.determination).toBe('not-applicable') expect(typo.reason).toContain('allowed by fail-open, NOT granted by the plan') }) it('names the subscription status that caused a fallback', () => { const cancelled = { profile: { id: 'u1' }, entitlement: { plan: 'pro', status: 'cancelled' } } const x = explainPlan(cancelled, 'pro', gating) expect(x.determination).toBe('fail') expect(x.reason).toContain('"cancelled" is not an access-granting status') expect(x.effectivePlan).toBe('free') }) it('past_due keeps access, and says so as a pass', () => { const pastDue = { profile: { id: 'u1' }, entitlement: { plan: 'pro', status: 'past_due' } } expect(explainPlan(pastDue, 'pro', gating).determination).toBe('pass') }) it('a signed-out principal is a denial, not an absence of assessment', () => { expect(explainPlan(null, 'pro', gating).determination).toBe('fail') }) it('cites the entitlement row behind each decision', () => { const text = formatAccess(pro, [{ feature: 'bulk export', minPlan: 'enterprise' }], gating, 'u1', { evidence: 'full' }) expect(text).toContain('entitlements#u1 (plan)') expect(text).toContain('entitlements#u1 (status)') }) it('every conclusion carries a citation', () => { const r = toReport(pro, [{ feature: 'bulk export', minPlan: 'enterprise' }], gating, 'u1') expect(validateReport(r)).toEqual([]) }) })