- Add frontend unit tests with Vitest for components, hooks, pages, and utilities - Add backend tests for webhooks, notifications, middleware, and edge cases - Add ForgotPassword, NotFound, and ResetPassword pages - Add migration for orphaned staff resources conversion - Add coverage directory to gitignore (generated reports) - Various bug fixes and improvements from previous work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
990 lines
30 KiB
TypeScript
990 lines
30 KiB
TypeScript
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',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|