How to Test Seat Limits, Invites, and Team Growth
Short answer
Seat bugs block team growth or grant free seats—both hurt ARR. Test at-limit invite rejection, seat reclaim after deactivate, and overage billing with orgs seeded at N−1, N, and N+1 seats. Probe active seat count and Stripe subscription quantity—not admin UI member list alone.
Part of Testing Guides by business flow.
Who this is for
B2B SaaS with per-seat pricing, team invites, and role-based membership synced to Stripe subscription quantity or internal billing.
Why testing seat licensing matters
- Revenue — 6th member on 5-seat plan without overage billing.
- Growth — at-limit invite fails without clear upgrade path.
- Security — deactivated user still consumes seat; re-invite blocked.
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| At limit | Plan allows 5 | 6th invite succeeds | Seed 5 members; probe reject |
| Pending invite | Counts toward limit? | Double booking | Probe pending + active seats |
| Deactivate reclaim | Seat not freed | Cannot re-invite | Probe count after deactivate |
| Overage billing | Auto-add seat | Stripe qty wrong | Probe subscription quantity |
| Downgrade seats | Too many members | Policy untested | Seed 10 users on 5-seat plan |
| Admin vs member | Admin exempt? | Wrong limit | Probe role rules |
| SSO JIT | Auto-provision | Seat burst | Seed IdP mock user batch |
| Transfer ownership | Seat double count | Probe org seats | |
| Guest vs full seat | Cheaper guest | Wrong billing | Probe seat_bucket |
| Annual true-up | Quarterly reconcile | Untested | Clock + probe true-up invoice |
| Remove pending invite | Frees seat | UI only | Probe pending count |
At-limit invite
await request.post('/api/test/seed-org', {
data: { runId, plan: 'pro', seatLimit: 5, activeMembers: 5 },
});
const adminToken = await getAdminToken(request, runId);
const invite = await request.post('/api/orgs/members/invite', {
headers: { Authorization: `Bearer ${adminToken}` },
data: { email: `sixth-${runId}@test.local` },
});
expect(invite.status()).toBe(409);
const seats = await request.get(`/api/test/probes/seats/${runId}`).then(r => r.json());
expect(seats.active).toBe(5);
expect(seats.available).toBe(0);
Seat reclaim
await request.post('/api/test/seed-org', {
data: { runId, seatLimit: 5, activeMembers: 5 },
});
await request.post(`/api/test/deactivate-member`, {
data: { runId, memberIndex: 0 },
});
await expect.poll(async () => {
return (await request.get(`/api/test/probes/seats/${runId}`).then(r => r.json())).active;
}).toBe(4);
const invite = await request.post('/api/orgs/members/invite', {
headers: { Authorization: `Bearer ${adminToken}` },
data: { email: `replacement-${runId}@test.local` },
});
expect(invite.ok()).toBeTruthy();
Overage billing
When prod auto-increments Stripe quantity:
await request.post('/api/test/seed-org', {
data: { runId, seatLimit: 5, allowOverage: true, activeMembers: 5 },
});
await acceptInviteFlow(page, `sixth-${runId}@test.local`);
await expect.poll(async () => {
const sub = await request.get(`/api/test/probes/subscriptions/${runId}`).then(r => r.json());
return sub.quantity;
}).toBe(6);
Link to RBAC and entitlements
Seat limits interact with feature entitlements and admin RBAC—probe API for invite permission vs billing limit separately.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Toast "limit reached" | Invite actually created | Probe member count |
| Skip deactivate reclaim | Stale seat pool | Reclaim scenario |
| UI member table | Out of sync with billing | Probe seats + Stripe qty |
| Shared org in CI | Parallel invites | Per-run org seed |
Example scenario
Situation: Admin on 5-seat plan tries to invite a sixth active member.
Expected outcome: Invite rejected or upgrade required; seat count stays 5; no silent overage unless policy allows.
Why UI-only automation breaks: Invite button disabled but API accepts POST /invite.
- Arrange: Seed org at seat limit with 5 active members.
- Act: POST invite or UI invite flow.
- Assert: Probe 409 or upgrade_required; seats.active=5; optional upgrade CTA.
TestChimp workflow: Track seat_bucket × plan_limit × role in TrueCoverage for Enterprise overage.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- Feature entitlements — plan features
- Admin RBAC — who can invite
- Subscriptions billing — quantity sync
External references
Frequently asked questions
How do I test seat limit enforcement?
Seed org at plan limit with active members, attempt invite in UI or API, probe returns error and member count unchanged—do not only check toast.
Do pending invites count toward the limit?
Match prod rules in probe—test both policies if product debated; document expected pending_seats in seed fixtures.
How do I test seat reclaim after deactivation?
Deactivate member, poll probe until active count drops, then successful invite for replacement email.
How is Stripe subscription quantity kept in sync?
After seat change, poll probe subscription quantity matches active billable seats—especially with overage auto-upgrade.
Downgrade with too many members?
Seed org over limit, attempt plan downgrade, probe blocks or forces member removal workflow.
Guest seats vs full seats?
Seed roles with different seat_bucket; probe billing counts only full seats if that is prod rule.
Starter vs Pro vs Enterprise have different seat rules—how do we track coverage?
TrueCoverage on seat_bucket × plan_limit × role. Run /testchimp evolve when plan limits change—link // @Scenario: per tier.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.