/** * @vitest-environment node */ import { schemaMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetUserOrganization, mockGetOrganizationSubscription, mockGetHighestPrioritySubscription, mockDbResults, mockFeatureFlags, } = vi.hoisted(() => { const mockGetUserOrganization = vi.fn() const mockGetOrganizationSubscription = vi.fn() const mockGetHighestPrioritySubscription = vi.fn() const mockDbResults: { value: any[] } = { value: [] } const mockFeatureFlags = { isBillingEnabled: true } return { mockGetUserOrganization, mockGetOrganizationSubscription, mockGetHighestPrioritySubscription, mockDbResults, mockFeatureFlags, } }) vi.mock('@sim/db', () => ({ db: { select: vi.fn().mockImplementation(() => { const chain: any = {} chain.from = vi.fn().mockReturnValue(chain) chain.where = vi.fn().mockReturnValue(chain) chain.limit = vi .fn() .mockImplementation(() => Promise.resolve(mockDbResults.value.shift() || [])) chain.then = vi.fn().mockImplementation((callback: (rows: any[]) => unknown) => { const result = mockDbResults.value.shift() || [] return Promise.resolve(callback ? callback(result) : result) }) return chain }), }, })) vi.mock('@sim/db/schema', () => schemaMock) vi.mock('@/lib/billing/organizations/membership', () => ({ getUserOrganization: mockGetUserOrganization, })) vi.mock('@/lib/billing/core/billing', () => ({ getOrganizationSubscription: mockGetOrganizationSubscription, })) vi.mock('@/lib/billing/core/plan', () => ({ getHighestPrioritySubscription: mockGetHighestPrioritySubscription, })) vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return mockFeatureFlags.isBillingEnabled }, })) import { getWorkspaceCreationPolicy, getWorkspaceInvitePolicy, WORKSPACE_MODE, } from '@/lib/workspaces/policy' import { UPGRADE_TO_INVITE_REASON } from '@/lib/workspaces/policy-constants' describe('getWorkspaceCreationPolicy', () => { beforeEach(() => { vi.clearAllMocks() mockDbResults.value = [] mockFeatureFlags.isBillingEnabled = true mockGetUserOrganization.mockResolvedValue(null) mockGetOrganizationSubscription.mockResolvedValue(null) mockGetHighestPrioritySubscription.mockResolvedValue(null) }) it('blocks free users once they already own one non-organization workspace', async () => { mockDbResults.value = [[{ value: 1 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) expect(result.canCreate).toBe(false) expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) expect(result.maxWorkspaces).toBe(1) expect(result.currentWorkspaceCount).toBe(1) }) it('allows pro users to create up to three personal workspaces', async () => { mockGetHighestPrioritySubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'pro_6000', status: 'active', }) mockDbResults.value = [[{ value: 2 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) expect(result.canCreate).toBe(true) expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) expect(result.maxWorkspaces).toBe(3) expect(result.currentWorkspaceCount).toBe(2) }) it('allows max users to create up to ten personal workspaces', async () => { mockGetHighestPrioritySubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'pro_25000', status: 'active', }) mockDbResults.value = [[{ value: 5 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) expect(result.canCreate).toBe(true) expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) expect(result.maxWorkspaces).toBe(10) expect(result.currentWorkspaceCount).toBe(5) }) it('blocks max users once they already own ten personal workspaces', async () => { mockGetHighestPrioritySubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'pro_25000', status: 'active', }) mockDbResults.value = [[{ value: 10 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) expect(result.canCreate).toBe(false) expect(result.maxWorkspaces).toBe(10) expect(result.currentWorkspaceCount).toBe(10) }) it('allows unlimited personal workspaces when billing is disabled', async () => { mockFeatureFlags.isBillingEnabled = false mockDbResults.value = [[{ value: 9 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1' }) expect(result.canCreate).toBe(true) expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) expect(result.maxWorkspaces).toBeNull() expect(result.currentWorkspaceCount).toBe(9) expect(mockGetHighestPrioritySubscription).not.toHaveBeenCalled() }) it('without pinning, a null active org falls back to the caller membership org', async () => { mockFeatureFlags.isBillingEnabled = false mockGetUserOrganization.mockResolvedValue({ organizationId: 'user-org', role: 'admin', memberId: 'member-1', }) mockDbResults.value = [[{ userId: 'owner-1' }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1', activeOrganizationId: null, }) expect(result.workspaceMode).toBe(WORKSPACE_MODE.ORGANIZATION) expect(result.organizationId).toBe('user-org') }) it('pins to the source org: a personal source (null) stays personal regardless of caller org', async () => { mockFeatureFlags.isBillingEnabled = false mockGetUserOrganization.mockResolvedValue({ organizationId: 'user-org', role: 'admin', memberId: 'member-1', }) mockDbResults.value = [[{ value: 0 }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1', activeOrganizationId: null, pinOrganization: true, }) expect(result.canCreate).toBe(true) expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL) expect(result.organizationId).toBeNull() expect(result.billedAccountUserId).toBe('user-1') }) it('allows org admins on a team plan to create organization workspaces', async () => { mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', role: 'admin', memberId: 'member-1', }) mockGetOrganizationSubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'team_6000', status: 'active', }) mockDbResults.value = [[{ userId: 'owner-1' }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1', activeOrganizationId: 'org-1', }) expect(result.canCreate).toBe(true) expect(result.workspaceMode).toBe(WORKSPACE_MODE.ORGANIZATION) expect(result.organizationId).toBe('org-1') expect(result.billedAccountUserId).toBe('owner-1') }) it('allows org admins to create organization workspaces when billing is disabled', async () => { mockFeatureFlags.isBillingEnabled = false mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', role: 'admin', memberId: 'member-1', }) mockDbResults.value = [[{ userId: 'owner-1' }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1', activeOrganizationId: 'org-1', }) expect(result.canCreate).toBe(true) expect(result.workspaceMode).toBe(WORKSPACE_MODE.ORGANIZATION) expect(result.organizationId).toBe('org-1') expect(result.billedAccountUserId).toBe('owner-1') expect(mockGetOrganizationSubscription).not.toHaveBeenCalled() }) it('blocks non-admin org members from creating organization workspaces', async () => { mockGetUserOrganization.mockResolvedValueOnce({ organizationId: 'org-1', role: 'member', memberId: 'member-1', }) mockGetOrganizationSubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'enterprise', status: 'active', }) mockDbResults.value = [[{ userId: 'owner-1' }]] const result = await getWorkspaceCreationPolicy({ userId: 'user-1', activeOrganizationId: 'org-1', }) expect(result.canCreate).toBe(false) expect(result.workspaceMode).toBe(WORKSPACE_MODE.ORGANIZATION) expect(result.reason).toContain('owners and admins') }) it('blocks users without org membership from creating workspaces in the active org context', async () => { mockDbResults.value = [[], [{ userId: 'owner-1' }]] const result = await getWorkspaceCreationPolicy({ userId: 'external-user-1', activeOrganizationId: 'org-1', }) expect(result.canCreate).toBe(false) expect(result.workspaceMode).toBe(WORKSPACE_MODE.ORGANIZATION) expect(result.organizationId).toBe('org-1') expect(result.billedAccountUserId).toBe('owner-1') expect(result.reason).toContain('owners and admins') expect(mockGetOrganizationSubscription).not.toHaveBeenCalled() expect(mockGetHighestPrioritySubscription).not.toHaveBeenCalled() }) }) describe('getWorkspaceInvitePolicy', () => { beforeEach(() => { vi.clearAllMocks() mockFeatureFlags.isBillingEnabled = true mockGetOrganizationSubscription.mockResolvedValue(null) mockGetHighestPrioritySubscription.mockResolvedValue(null) }) const baseState = { workspaceMode: WORKSPACE_MODE.PERSONAL, organizationId: null, billedAccountUserId: 'owner-1', ownerId: 'owner-1', } as const it('allows invites unconditionally when billing is disabled', async () => { mockFeatureFlags.isBillingEnabled = false const result = await getWorkspaceInvitePolicy(baseState) expect(result.allowed).toBe(true) expect(result.upgradeRequired).toBe(false) expect(mockGetHighestPrioritySubscription).not.toHaveBeenCalled() }) it('blocks free personal workspaces with an upgrade prompt', async () => { const result = await getWorkspaceInvitePolicy(baseState) expect(result.allowed).toBe(false) expect(result.upgradeRequired).toBe(true) expect(result.reason).toBe(UPGRADE_TO_INVITE_REASON) }) it('allows pro personal workspaces and defers the team upgrade to acceptance', async () => { mockGetHighestPrioritySubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'pro_6000', status: 'active', }) const result = await getWorkspaceInvitePolicy(baseState) expect(result.allowed).toBe(true) expect(result.requiresSeat).toBe(false) expect(result.upgradeRequired).toBe(false) }) it('allows team org workspaces without an invite-time seat gate', async () => { mockGetOrganizationSubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'team_6000', status: 'active', }) const result = await getWorkspaceInvitePolicy({ ...baseState, workspaceMode: WORKSPACE_MODE.ORGANIZATION, organizationId: 'org-1', }) expect(result.allowed).toBe(true) expect(result.requiresSeat).toBe(false) expect(result.organizationId).toBe('org-1') }) it('keeps the fixed-seat gate for enterprise org workspaces', async () => { mockGetOrganizationSubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'enterprise', status: 'active', }) const result = await getWorkspaceInvitePolicy({ ...baseState, workspaceMode: WORKSPACE_MODE.ORGANIZATION, organizationId: 'org-1', }) expect(result.allowed).toBe(true) expect(result.requiresSeat).toBe(true) }) it('blocks org workspaces whose organization has no usable subscription', async () => { mockGetOrganizationSubscription.mockResolvedValueOnce(null) const result = await getWorkspaceInvitePolicy({ ...baseState, workspaceMode: WORKSPACE_MODE.ORGANIZATION, organizationId: 'org-1', }) expect(result.allowed).toBe(false) expect(result.upgradeRequired).toBe(true) }) it('blocks org workspaces without an organization id', async () => { const result = await getWorkspaceInvitePolicy({ ...baseState, workspaceMode: WORKSPACE_MODE.ORGANIZATION, }) expect(result.allowed).toBe(false) expect(result.upgradeRequired).toBe(true) }) it('allows grandfathered workspaces when the billed user has a team plan', async () => { mockGetHighestPrioritySubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'team_6000', status: 'active', }) const result = await getWorkspaceInvitePolicy({ ...baseState, workspaceMode: WORKSPACE_MODE.GRANDFATHERED_SHARED, }) expect(result.allowed).toBe(true) expect(result.upgradeRequired).toBe(false) expect(mockGetHighestPrioritySubscription).toHaveBeenCalledWith('owner-1') }) it('allows grandfathered workspaces when the billed user has a pro plan', async () => { mockGetHighestPrioritySubscription.mockResolvedValueOnce({ id: 'sub-1', plan: 'pro_6000', status: 'active', }) const result = await getWorkspaceInvitePolicy({ ...baseState, workspaceMode: WORKSPACE_MODE.GRANDFATHERED_SHARED, }) expect(result.allowed).toBe(true) expect(result.upgradeRequired).toBe(false) }) it('blocks grandfathered workspaces when the billed user is on a free plan', async () => { mockGetHighestPrioritySubscription.mockResolvedValueOnce(null) const result = await getWorkspaceInvitePolicy({ ...baseState, workspaceMode: WORKSPACE_MODE.GRANDFATHERED_SHARED, }) expect(result.allowed).toBe(false) expect(result.upgradeRequired).toBe(true) expect(result.reason).toBe(UPGRADE_TO_INVITE_REASON) }) })