tiers.test.ts90 lines · main
1import { describe, expect, test } from 'bun:test';
2
3import { RATE_LIMITS_BY_TIER, RATE_LIMIT_WINDOW_MS, TIERS } from './tiers.js';
4
5describe('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
40describe('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
59describe('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});