tiers.test.ts90 lines · main
| 1 | import { describe, expect, test } from 'bun:test'; |
| 2 | |
| 3 | import { RATE_LIMITS_BY_TIER, RATE_LIMIT_WINDOW_MS, TIERS } from './tiers.js'; |
| 4 | |
| 5 | describe('RATE_LIMITS_BY_TIER', () => { |
| 6 | test('every scope has a positive limit for every tier', () => { |
| 7 | for (const [scope, byTier] of Object.entries(RATE_LIMITS_BY_TIER)) { |
| 8 | for (const [tier, limit] of Object.entries(byTier)) { |
| 9 | expect(limit).toBeGreaterThan(0); |
| 10 | expect(Number.isInteger(limit)).toBe(true); |
| 11 | // Surface helpful context if the assertion fails. |
| 12 | if (!Number.isInteger(limit) || limit <= 0) { |
| 13 | throw new Error(`bad limit for ${scope}/${tier}: ${limit}`); |
| 14 | } |
| 15 | } |
| 16 | } |
| 17 | }); |
| 18 | |
| 19 | test('limits are monotonically non-decreasing free → pro → team', () => { |
| 20 | for (const byTier of Object.values(RATE_LIMITS_BY_TIER)) { |
| 21 | expect(byTier.free).toBeLessThanOrEqual(byTier.pro); |
| 22 | expect(byTier.pro).toBeLessThanOrEqual(byTier.team); |
| 23 | } |
| 24 | }); |
| 25 | |
| 26 | test('covers read + invoke + deploy + mutate scopes', () => { |
| 27 | expect(Object.keys(RATE_LIMITS_BY_TIER).sort()).toEqual([ |
| 28 | 'deploy', |
| 29 | 'invoke', |
| 30 | 'mutate', |
| 31 | 'read', |
| 32 | ]); |
| 33 | }); |
| 34 | |
| 35 | test('window is exactly one minute', () => { |
| 36 | expect(RATE_LIMIT_WINDOW_MS).toBe(60_000); |
| 37 | }); |
| 38 | }); |
| 39 | |
| 40 | describe('TIERS structural caps', () => { |
| 41 | test('every tier has positive caps', () => { |
| 42 | for (const limits of Object.values(TIERS)) { |
| 43 | expect(limits.projectsPerOrg).toBeGreaterThan(0); |
| 44 | expect(limits.functionsPerProject).toBeGreaterThan(0); |
| 45 | expect(limits.invokesPerMonth).toBeGreaterThan(0); |
| 46 | } |
| 47 | }); |
| 48 | |
| 49 | test('structural caps are monotonically non-decreasing', () => { |
| 50 | expect(TIERS.free.projectsPerOrg).toBeLessThanOrEqual(TIERS.pro.projectsPerOrg); |
| 51 | expect(TIERS.pro.projectsPerOrg).toBeLessThanOrEqual(TIERS.team.projectsPerOrg); |
| 52 | expect(TIERS.free.functionsPerProject).toBeLessThanOrEqual(TIERS.pro.functionsPerProject); |
| 53 | expect(TIERS.pro.functionsPerProject).toBeLessThanOrEqual(TIERS.team.functionsPerProject); |
| 54 | expect(TIERS.free.invokesPerMonth).toBeLessThanOrEqual(TIERS.pro.invokesPerMonth); |
| 55 | expect(TIERS.pro.invokesPerMonth).toBeLessThanOrEqual(TIERS.team.invokesPerMonth); |
| 56 | }); |
| 57 | }); |
| 58 | |
| 59 | describe('polar subscription → tier collapse', () => { |
| 60 | // Mirrors the rule in upsertSubscriptionFromPolar — only 'active' and |
| 61 | // 'trialing' get the paid tier; cancelled / past-due collapse to free. |
| 62 | // Pinning the matrix here so a policy change is caught in CI before it |
| 63 | // reaches production billing. |
| 64 | function effectiveTier( |
| 65 | paidTier: 'pro' | 'team', |
| 66 | status: 'active' | 'trialing' | 'past_due' | 'canceled', |
| 67 | ): 'free' | 'pro' | 'team' { |
| 68 | return status === 'canceled' || status === 'past_due' ? 'free' : paidTier; |
| 69 | } |
| 70 | |
| 71 | test('active subscription keeps the paid tier', () => { |
| 72 | expect(effectiveTier('pro', 'active')).toBe('pro'); |
| 73 | expect(effectiveTier('team', 'active')).toBe('team'); |
| 74 | }); |
| 75 | |
| 76 | test('trialing keeps the paid tier (paid behaviour during trial)', () => { |
| 77 | expect(effectiveTier('pro', 'trialing')).toBe('pro'); |
| 78 | expect(effectiveTier('team', 'trialing')).toBe('team'); |
| 79 | }); |
| 80 | |
| 81 | test('past_due collapses to free — delinquent users lose paid caps', () => { |
| 82 | expect(effectiveTier('pro', 'past_due')).toBe('free'); |
| 83 | expect(effectiveTier('team', 'past_due')).toBe('free'); |
| 84 | }); |
| 85 | |
| 86 | test('canceled collapses to free', () => { |
| 87 | expect(effectiveTier('pro', 'canceled')).toBe('free'); |
| 88 | expect(effectiveTier('team', 'canceled')).toBe('free'); |
| 89 | }); |
| 90 | }); |