|
1 | 1 | /** |
2 | 2 | * @vitest-environment node |
3 | 3 | */ |
4 | | -import { auditMock, createMockRequest, dbChainMockFns, resetDbChainMock } from '@sim/testing' |
5 | | -import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 4 | +import { |
| 5 | + auditMock, |
| 6 | + createMockRequest, |
| 7 | + dbChainMockFns, |
| 8 | + resetDbChainMock, |
| 9 | + resetEnvFlagsMock, |
| 10 | + setEnvFlags, |
| 11 | +} from '@sim/testing' |
| 12 | +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' |
6 | 13 |
|
7 | 14 | const { |
8 | 15 | mockGetUserOrganization, |
@@ -125,10 +132,14 @@ const request = createMockRequest( |
125 | 132 | 'http://localhost/api/workspaces/invitations/batch' |
126 | 133 | ) |
127 | 134 |
|
| 135 | +afterAll(resetEnvFlagsMock) |
| 136 | + |
128 | 137 | describe('createWorkspaceInvitation', () => { |
129 | 138 | beforeEach(() => { |
130 | 139 | vi.clearAllMocks() |
131 | 140 | resetDbChainMock() |
| 141 | + /** Production default; the billing-disabled case opts out explicitly. */ |
| 142 | + setEnvFlags({ isBillingEnabled: true }) |
132 | 143 | mockGrantWorkspaceAccessDirectly.mockResolvedValue({ outcome: 'added', permission: 'write' }) |
133 | 144 | mockCreatePendingInvitation.mockResolvedValue({ |
134 | 145 | invitationId: 'inv-1', |
@@ -259,6 +270,31 @@ describe('createWorkspaceInvitation', () => { |
259 | 270 | ) |
260 | 271 | }) |
261 | 272 |
|
| 273 | + it('allows an external invite with billing disabled, where nobody has a plan', async () => { |
| 274 | + /** |
| 275 | + * The paid-plan requirement is seat economics: an external collaborator takes |
| 276 | + * no seat, so somebody else must be paying for them. With billing off there |
| 277 | + * are no seats and no subscriptions, so every account reads as free — |
| 278 | + * enforcing it would leave a self-hosted deployment no way to grant |
| 279 | + * workspace-only access without an organization join and a workspace sweep. |
| 280 | + */ |
| 281 | + setEnvFlags({ isBillingEnabled: false }) |
| 282 | + queueWhereResponses([[{ id: 'user-9', email: 'selfhost@example.com' }], []]) |
| 283 | + mockGetUserOrganization.mockResolvedValueOnce(null) |
| 284 | + |
| 285 | + const result = await createWorkspaceInvitation({ |
| 286 | + context: makeContext(), |
| 287 | + email: 'selfhost@example.com', |
| 288 | + permission: 'write', |
| 289 | + membership: 'external', |
| 290 | + request, |
| 291 | + }) |
| 292 | + |
| 293 | + expect(result.membershipIntent).toBe('external') |
| 294 | + /** Short-circuits before the plan lookup — there is nothing to look up. */ |
| 295 | + expect(mockGetInvitePlanCategoryForUser).not.toHaveBeenCalled() |
| 296 | + }) |
| 297 | + |
262 | 298 | it('rejects an explicit external invite for an invitee with no paid plan', async () => { |
263 | 299 | queueWhereResponses([[{ id: 'user-5', email: 'free@example.com' }], []]) |
264 | 300 | mockGetUserOrganization.mockResolvedValueOnce(null) |
|
0 commit comments