Skip to main content

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

ScenarioEdge caseWhy tests breakApproach
At limitPlan allows 56th invite succeedsSeed 5 members; probe reject
Pending inviteCounts toward limit?Double bookingProbe pending + active seats
Deactivate reclaimSeat not freedCannot re-inviteProbe count after deactivate
Overage billingAuto-add seatStripe qty wrongProbe subscription quantity
Downgrade seatsToo many membersPolicy untestedSeed 10 users on 5-seat plan
Admin vs memberAdmin exempt?Wrong limitProbe role rules
SSO JITAuto-provisionSeat burstSeed IdP mock user batch
Transfer ownershipSeat double countProbe org seats
Guest vs full seatCheaper guestWrong billingProbe seat_bucket
Annual true-upQuarterly reconcileUntestedClock + probe true-up invoice
Remove pending inviteFrees seatUI onlyProbe 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);

Seat limits interact with feature entitlements and admin RBAC—probe API for invite permission vs billing limit separately.

Anti-patterns

Anti-patternWhy it failsBetter approach
Toast "limit reached"Invite actually createdProbe member count
Skip deactivate reclaimStale seat poolReclaim scenario
UI member tableOut of sync with billingProbe seats + Stripe qty
Shared org in CIParallel invitesPer-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.

  1. Arrange: Seed org at seat limit with 5 active members.
  2. Act: POST invite or UI invite flow.
  3. 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).

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.

Start free on TestChimp · Book a demo