import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock apiClient vi.mock('../client', () => ({ default: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn(), }, })); import { getBusinesses, updateBusiness, createBusiness, deleteBusiness, getUsers, getBusinessUsers, verifyUserEmail, getTenantInvitations, createTenantInvitation, resendTenantInvitation, cancelTenantInvitation, getInvitationByToken, acceptInvitation, type PlatformBusiness, type PlatformBusinessUpdate, type PlatformBusinessCreate, type PlatformUser, type TenantInvitation, type TenantInvitationCreate, type TenantInvitationDetail, type TenantInvitationAccept, } from '../platform'; import apiClient from '../client'; describe('platform API', () => { beforeEach(() => { vi.clearAllMocks(); }); // ============================================================================ // Business Management // ============================================================================ describe('getBusinesses', () => { it('fetches all businesses from API', async () => { const mockBusinesses: PlatformBusiness[] = [ { id: 1, name: 'Acme Corp', subdomain: 'acme', tier: 'PROFESSIONAL', is_active: true, created_on: '2025-01-01T00:00:00Z', user_count: 5, owner: { id: 10, username: 'john', full_name: 'John Doe', email: 'john@acme.com', role: 'owner', email_verified: true, }, max_users: 20, max_resources: 50, contact_email: 'contact@acme.com', phone: '555-1234', can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: false, can_white_label: false, can_api_access: true, }, { id: 2, name: 'Beta LLC', subdomain: 'beta', tier: 'STARTER', is_active: true, created_on: '2025-01-02T00:00:00Z', user_count: 2, owner: null, max_users: 5, max_resources: 10, can_manage_oauth_credentials: false, can_accept_payments: false, can_use_custom_domain: false, can_white_label: false, can_api_access: false, }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockBusinesses }); const result = await getBusinesses(); expect(apiClient.get).toHaveBeenCalledWith('/platform/businesses/'); expect(result).toEqual(mockBusinesses); expect(result).toHaveLength(2); expect(result[0].name).toBe('Acme Corp'); expect(result[1].owner).toBeNull(); }); it('returns empty array when no businesses exist', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const result = await getBusinesses(); expect(result).toEqual([]); }); }); describe('updateBusiness', () => { it('updates a business with full data', async () => { const businessId = 1; const updateData: PlatformBusinessUpdate = { name: 'Updated Name', is_active: false, subscription_tier: 'ENTERPRISE', max_users: 100, max_resources: 500, can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: true, can_white_label: true, can_api_access: true, }; const mockResponse: PlatformBusiness = { id: 1, name: 'Updated Name', subdomain: 'acme', tier: 'ENTERPRISE', is_active: false, created_on: '2025-01-01T00:00:00Z', user_count: 5, owner: null, max_users: 100, max_resources: 500, can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: true, can_white_label: true, can_api_access: true, }; vi.mocked(apiClient.patch).mockResolvedValue({ data: mockResponse }); const result = await updateBusiness(businessId, updateData); expect(apiClient.patch).toHaveBeenCalledWith( '/platform/businesses/1/', updateData ); expect(result).toEqual(mockResponse); expect(result.name).toBe('Updated Name'); expect(result.is_active).toBe(false); }); it('updates a business with partial data', async () => { const businessId = 2; const updateData: PlatformBusinessUpdate = { is_active: true, }; const mockResponse: PlatformBusiness = { id: 2, name: 'Beta LLC', subdomain: 'beta', tier: 'STARTER', is_active: true, created_on: '2025-01-02T00:00:00Z', user_count: 2, owner: null, max_users: 5, max_resources: 10, can_manage_oauth_credentials: false, can_accept_payments: false, can_use_custom_domain: false, can_white_label: false, can_api_access: false, }; vi.mocked(apiClient.patch).mockResolvedValue({ data: mockResponse }); const result = await updateBusiness(businessId, updateData); expect(apiClient.patch).toHaveBeenCalledWith( '/platform/businesses/2/', updateData ); expect(result.is_active).toBe(true); }); it('updates only specific permissions', async () => { const businessId = 3; const updateData: PlatformBusinessUpdate = { can_accept_payments: true, can_api_access: true, }; const mockResponse: PlatformBusiness = { id: 3, name: 'Gamma Inc', subdomain: 'gamma', tier: 'PROFESSIONAL', is_active: true, created_on: '2025-01-03T00:00:00Z', user_count: 10, owner: null, max_users: 20, max_resources: 50, can_manage_oauth_credentials: false, can_accept_payments: true, can_use_custom_domain: false, can_white_label: false, can_api_access: true, }; vi.mocked(apiClient.patch).mockResolvedValue({ data: mockResponse }); await updateBusiness(businessId, updateData); expect(apiClient.patch).toHaveBeenCalledWith( '/platform/businesses/3/', updateData ); }); }); describe('createBusiness', () => { it('creates a business with minimal data', async () => { const createData: PlatformBusinessCreate = { name: 'New Business', subdomain: 'newbiz', }; const mockResponse: PlatformBusiness = { id: 10, name: 'New Business', subdomain: 'newbiz', tier: 'FREE', is_active: true, created_on: '2025-01-15T00:00:00Z', user_count: 0, owner: null, max_users: 3, max_resources: 5, can_manage_oauth_credentials: false, can_accept_payments: false, can_use_custom_domain: false, can_white_label: false, can_api_access: false, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await createBusiness(createData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/businesses/', createData ); expect(result).toEqual(mockResponse); expect(result.id).toBe(10); expect(result.subdomain).toBe('newbiz'); }); it('creates a business with full data including owner', async () => { const createData: PlatformBusinessCreate = { name: 'Premium Business', subdomain: 'premium', subscription_tier: 'ENTERPRISE', is_active: true, max_users: 100, max_resources: 500, contact_email: 'contact@premium.com', phone: '555-9999', can_manage_oauth_credentials: true, owner_email: 'owner@premium.com', owner_name: 'Jane Smith', owner_password: 'secure-password', }; const mockResponse: PlatformBusiness = { id: 11, name: 'Premium Business', subdomain: 'premium', tier: 'ENTERPRISE', is_active: true, created_on: '2025-01-15T10:00:00Z', user_count: 1, owner: { id: 20, username: 'owner@premium.com', full_name: 'Jane Smith', email: 'owner@premium.com', role: 'owner', email_verified: false, }, max_users: 100, max_resources: 500, contact_email: 'contact@premium.com', phone: '555-9999', can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: true, can_white_label: true, can_api_access: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await createBusiness(createData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/businesses/', createData ); expect(result.owner).not.toBeNull(); expect(result.owner?.email).toBe('owner@premium.com'); }); it('creates a business with custom tier and limits', async () => { const createData: PlatformBusinessCreate = { name: 'Custom Business', subdomain: 'custom', subscription_tier: 'PROFESSIONAL', max_users: 50, max_resources: 100, }; const mockResponse: PlatformBusiness = { id: 12, name: 'Custom Business', subdomain: 'custom', tier: 'PROFESSIONAL', is_active: true, created_on: '2025-01-15T12:00:00Z', user_count: 0, owner: null, max_users: 50, max_resources: 100, can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: false, can_white_label: false, can_api_access: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await createBusiness(createData); expect(result.max_users).toBe(50); expect(result.max_resources).toBe(100); }); }); describe('deleteBusiness', () => { it('deletes a business by ID', async () => { const businessId = 5; vi.mocked(apiClient.delete).mockResolvedValue({}); await deleteBusiness(businessId); expect(apiClient.delete).toHaveBeenCalledWith('/platform/businesses/5/'); }); it('handles deletion with no response data', async () => { const businessId = 10; vi.mocked(apiClient.delete).mockResolvedValue({ data: undefined }); const result = await deleteBusiness(businessId); expect(apiClient.delete).toHaveBeenCalledWith('/platform/businesses/10/'); expect(result).toBeUndefined(); }); }); // ============================================================================ // User Management // ============================================================================ describe('getUsers', () => { it('fetches all users from API', async () => { const mockUsers: PlatformUser[] = [ { id: 1, email: 'admin@platform.com', username: 'admin', name: 'Platform Admin', role: 'superuser', is_active: true, is_staff: true, is_superuser: true, email_verified: true, business: null, date_joined: '2024-01-01T00:00:00Z', last_login: '2025-01-15T10:00:00Z', }, { id: 2, email: 'user@acme.com', username: 'user1', name: 'Acme User', role: 'staff', is_active: true, is_staff: false, is_superuser: false, email_verified: true, business: 1, business_name: 'Acme Corp', business_subdomain: 'acme', date_joined: '2024-06-01T00:00:00Z', last_login: '2025-01-14T15:30:00Z', }, { id: 3, email: 'inactive@example.com', username: 'inactive', is_active: false, is_staff: false, is_superuser: false, email_verified: false, business: null, date_joined: '2024-03-15T00:00:00Z', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockUsers }); const result = await getUsers(); expect(apiClient.get).toHaveBeenCalledWith('/platform/users/'); expect(result).toEqual(mockUsers); expect(result).toHaveLength(3); expect(result[0].is_superuser).toBe(true); expect(result[1].business_name).toBe('Acme Corp'); expect(result[2].is_active).toBe(false); }); it('returns empty array when no users exist', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const result = await getUsers(); expect(result).toEqual([]); }); }); describe('getBusinessUsers', () => { it('fetches users for a specific business', async () => { const businessId = 1; const mockUsers: PlatformUser[] = [ { id: 10, email: 'owner@acme.com', username: 'owner', name: 'John Doe', role: 'owner', is_active: true, is_staff: false, is_superuser: false, email_verified: true, business: 1, business_name: 'Acme Corp', business_subdomain: 'acme', date_joined: '2024-01-01T00:00:00Z', last_login: '2025-01-15T09:00:00Z', }, { id: 11, email: 'staff@acme.com', username: 'staff1', name: 'Jane Smith', role: 'staff', is_active: true, is_staff: false, is_superuser: false, email_verified: true, business: 1, business_name: 'Acme Corp', business_subdomain: 'acme', date_joined: '2024-03-01T00:00:00Z', last_login: '2025-01-14T16:00:00Z', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockUsers }); const result = await getBusinessUsers(businessId); expect(apiClient.get).toHaveBeenCalledWith('/platform/users/?business=1'); expect(result).toEqual(mockUsers); expect(result).toHaveLength(2); expect(result.every(u => u.business === 1)).toBe(true); }); it('returns empty array when business has no users', async () => { const businessId = 99; vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const result = await getBusinessUsers(businessId); expect(apiClient.get).toHaveBeenCalledWith('/platform/users/?business=99'); expect(result).toEqual([]); }); it('handles different business IDs correctly', async () => { const businessId = 5; vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); await getBusinessUsers(businessId); expect(apiClient.get).toHaveBeenCalledWith('/platform/users/?business=5'); }); }); describe('verifyUserEmail', () => { it('verifies a user email by ID', async () => { const userId = 10; vi.mocked(apiClient.post).mockResolvedValue({}); await verifyUserEmail(userId); expect(apiClient.post).toHaveBeenCalledWith('/platform/users/10/verify_email/'); }); it('handles verification with no response data', async () => { const userId = 25; vi.mocked(apiClient.post).mockResolvedValue({ data: undefined }); const result = await verifyUserEmail(userId); expect(apiClient.post).toHaveBeenCalledWith('/platform/users/25/verify_email/'); expect(result).toBeUndefined(); }); }); // ============================================================================ // Tenant Invitations // ============================================================================ describe('getTenantInvitations', () => { it('fetches all tenant invitations from API', async () => { const mockInvitations: TenantInvitation[] = [ { id: 1, email: 'newclient@example.com', token: 'abc123token', status: 'PENDING', suggested_business_name: 'New Client Corp', subscription_tier: 'PROFESSIONAL', custom_max_users: 50, custom_max_resources: 100, permissions: { can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: false, can_white_label: false, can_api_access: true, }, personal_message: 'Welcome to our platform!', invited_by: 1, invited_by_email: 'admin@platform.com', created_at: '2025-01-10T10:00:00Z', expires_at: '2025-01-24T10:00:00Z', accepted_at: null, created_tenant: null, created_tenant_name: null, created_user: null, created_user_email: null, }, { id: 2, email: 'accepted@example.com', token: 'xyz789token', status: 'ACCEPTED', suggested_business_name: 'Accepted Business', subscription_tier: 'STARTER', custom_max_users: null, custom_max_resources: null, permissions: {}, personal_message: '', invited_by: 1, invited_by_email: 'admin@platform.com', created_at: '2025-01-05T08:00:00Z', expires_at: '2025-01-19T08:00:00Z', accepted_at: '2025-01-06T12:00:00Z', created_tenant: 5, created_tenant_name: 'Accepted Business', created_user: 15, created_user_email: 'accepted@example.com', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockInvitations }); const result = await getTenantInvitations(); expect(apiClient.get).toHaveBeenCalledWith('/platform/tenant-invitations/'); expect(result).toEqual(mockInvitations); expect(result).toHaveLength(2); expect(result[0].status).toBe('PENDING'); expect(result[1].status).toBe('ACCEPTED'); }); it('returns empty array when no invitations exist', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const result = await getTenantInvitations(); expect(result).toEqual([]); }); }); describe('createTenantInvitation', () => { it('creates a tenant invitation with minimal data', async () => { const createData: TenantInvitationCreate = { email: 'client@example.com', subscription_tier: 'STARTER', }; const mockResponse: TenantInvitation = { id: 10, email: 'client@example.com', token: 'generated-token-123', status: 'PENDING', suggested_business_name: '', subscription_tier: 'STARTER', custom_max_users: null, custom_max_resources: null, permissions: {}, personal_message: '', invited_by: 1, invited_by_email: 'admin@platform.com', created_at: '2025-01-15T14:00:00Z', expires_at: '2025-01-29T14:00:00Z', accepted_at: null, created_tenant: null, created_tenant_name: null, created_user: null, created_user_email: null, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await createTenantInvitation(createData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/', createData ); expect(result).toEqual(mockResponse); expect(result.email).toBe('client@example.com'); expect(result.status).toBe('PENDING'); }); it('creates a tenant invitation with full data', async () => { const createData: TenantInvitationCreate = { email: 'vip@example.com', suggested_business_name: 'VIP Corp', subscription_tier: 'ENTERPRISE', custom_max_users: 500, custom_max_resources: 1000, permissions: { can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: true, can_white_label: true, can_api_access: true, }, personal_message: 'Welcome to our premium tier!', }; const mockResponse: TenantInvitation = { id: 11, email: 'vip@example.com', token: 'vip-token-456', status: 'PENDING', suggested_business_name: 'VIP Corp', subscription_tier: 'ENTERPRISE', custom_max_users: 500, custom_max_resources: 1000, permissions: { can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: true, can_white_label: true, can_api_access: true, }, personal_message: 'Welcome to our premium tier!', invited_by: 1, invited_by_email: 'admin@platform.com', created_at: '2025-01-15T15:00:00Z', expires_at: '2025-01-29T15:00:00Z', accepted_at: null, created_tenant: null, created_tenant_name: null, created_user: null, created_user_email: null, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await createTenantInvitation(createData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/', createData ); expect(result.suggested_business_name).toBe('VIP Corp'); expect(result.custom_max_users).toBe(500); expect(result.permissions.can_white_label).toBe(true); }); it('creates invitation with partial permissions', async () => { const createData: TenantInvitationCreate = { email: 'partial@example.com', subscription_tier: 'PROFESSIONAL', permissions: { can_accept_payments: true, }, }; const mockResponse: TenantInvitation = { id: 12, email: 'partial@example.com', token: 'partial-token', status: 'PENDING', suggested_business_name: '', subscription_tier: 'PROFESSIONAL', custom_max_users: null, custom_max_resources: null, permissions: { can_accept_payments: true, }, personal_message: '', invited_by: 1, invited_by_email: 'admin@platform.com', created_at: '2025-01-15T16:00:00Z', expires_at: '2025-01-29T16:00:00Z', accepted_at: null, created_tenant: null, created_tenant_name: null, created_user: null, created_user_email: null, }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await createTenantInvitation(createData); expect(result.permissions.can_accept_payments).toBe(true); }); }); describe('resendTenantInvitation', () => { it('resends a tenant invitation by ID', async () => { const invitationId = 5; vi.mocked(apiClient.post).mockResolvedValue({}); await resendTenantInvitation(invitationId); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/5/resend/' ); }); it('handles resend with no response data', async () => { const invitationId = 10; vi.mocked(apiClient.post).mockResolvedValue({ data: undefined }); const result = await resendTenantInvitation(invitationId); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/10/resend/' ); expect(result).toBeUndefined(); }); }); describe('cancelTenantInvitation', () => { it('cancels a tenant invitation by ID', async () => { const invitationId = 7; vi.mocked(apiClient.post).mockResolvedValue({}); await cancelTenantInvitation(invitationId); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/7/cancel/' ); }); it('handles cancellation with no response data', async () => { const invitationId = 15; vi.mocked(apiClient.post).mockResolvedValue({ data: undefined }); const result = await cancelTenantInvitation(invitationId); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/15/cancel/' ); expect(result).toBeUndefined(); }); }); describe('getInvitationByToken', () => { it('fetches invitation details by token', async () => { const token = 'abc123token'; const mockInvitation: TenantInvitationDetail = { email: 'invited@example.com', suggested_business_name: 'Invited Corp', subscription_tier: 'PROFESSIONAL', effective_max_users: 20, effective_max_resources: 50, permissions: { can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: false, can_white_label: false, can_api_access: true, }, expires_at: '2025-01-30T12:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockInvitation }); const result = await getInvitationByToken(token); expect(apiClient.get).toHaveBeenCalledWith( '/platform/tenant-invitations/token/abc123token/' ); expect(result).toEqual(mockInvitation); expect(result.email).toBe('invited@example.com'); expect(result.effective_max_users).toBe(20); }); it('handles tokens with special characters', async () => { const token = 'token-with-dashes_and_underscores'; const mockInvitation: TenantInvitationDetail = { email: 'test@example.com', suggested_business_name: 'Test', subscription_tier: 'FREE', effective_max_users: 3, effective_max_resources: 5, permissions: {}, expires_at: '2025-02-01T00:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockInvitation }); await getInvitationByToken(token); expect(apiClient.get).toHaveBeenCalledWith( '/platform/tenant-invitations/token/token-with-dashes_and_underscores/' ); }); it('fetches invitation with custom limits', async () => { const token = 'custom-limits-token'; const mockInvitation: TenantInvitationDetail = { email: 'custom@example.com', suggested_business_name: 'Custom Business', subscription_tier: 'ENTERPRISE', effective_max_users: 1000, effective_max_resources: 5000, permissions: { can_manage_oauth_credentials: true, can_accept_payments: true, can_use_custom_domain: true, can_white_label: true, can_api_access: true, }, expires_at: '2025-03-01T12:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockInvitation }); const result = await getInvitationByToken(token); expect(result.effective_max_users).toBe(1000); expect(result.effective_max_resources).toBe(5000); }); }); describe('acceptInvitation', () => { it('accepts an invitation with full data', async () => { const token = 'accept-token-123'; const acceptData: TenantInvitationAccept = { email: 'newowner@example.com', password: 'secure-password', first_name: 'John', last_name: 'Doe', business_name: 'New Business LLC', subdomain: 'newbiz', contact_email: 'contact@newbiz.com', phone: '555-1234', }; const mockResponse = { detail: 'Invitation accepted successfully. Your account has been created.', }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await acceptInvitation(token, acceptData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/token/accept-token-123/accept/', acceptData ); expect(result).toEqual(mockResponse); expect(result.detail).toContain('successfully'); }); it('accepts an invitation with minimal data', async () => { const token = 'minimal-token'; const acceptData: TenantInvitationAccept = { email: 'minimal@example.com', password: 'password123', first_name: 'Jane', last_name: 'Smith', business_name: 'Minimal Business', subdomain: 'minimal', }; const mockResponse = { detail: 'Account created successfully.', }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const result = await acceptInvitation(token, acceptData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/token/minimal-token/accept/', acceptData ); expect(result.detail).toBe('Account created successfully.'); }); it('handles acceptance with optional contact fields', async () => { const token = 'optional-fields-token'; const acceptData: TenantInvitationAccept = { email: 'test@example.com', password: 'testpass', first_name: 'Test', last_name: 'User', business_name: 'Test Business', subdomain: 'testbiz', contact_email: 'info@testbiz.com', }; const mockResponse = { detail: 'Welcome to the platform!', }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); await acceptInvitation(token, acceptData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/token/optional-fields-token/accept/', expect.objectContaining({ contact_email: 'info@testbiz.com', }) ); }); it('preserves all required fields in request', async () => { const token = 'complete-token'; const acceptData: TenantInvitationAccept = { email: 'complete@example.com', password: 'strong-password-123', first_name: 'Complete', last_name: 'User', business_name: 'Complete Business Corp', subdomain: 'complete', contact_email: 'support@complete.com', phone: '555-9876', }; vi.mocked(apiClient.post).mockResolvedValue({ data: { detail: 'Success' }, }); await acceptInvitation(token, acceptData); expect(apiClient.post).toHaveBeenCalledWith( '/platform/tenant-invitations/token/complete-token/accept/', expect.objectContaining({ email: 'complete@example.com', password: 'strong-password-123', first_name: 'Complete', last_name: 'User', business_name: 'Complete Business Corp', subdomain: 'complete', contact_email: 'support@complete.com', phone: '555-9876', }) ); }); }); });