Files
smoothschedule/frontend/src/hooks/__tests__/usePlatform.test.ts
poduck 8dc2248f1f feat: Add comprehensive test suite and misc improvements
- 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>
2025-12-08 02:36:46 -05:00

1197 lines
35 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
// Mock platform API
vi.mock('../../api/platform', () => ({
getBusinesses: vi.fn(),
getUsers: vi.fn(),
getBusinessUsers: vi.fn(),
updateBusiness: vi.fn(),
createBusiness: vi.fn(),
deleteBusiness: vi.fn(),
getTenantInvitations: vi.fn(),
createTenantInvitation: vi.fn(),
resendTenantInvitation: vi.fn(),
cancelTenantInvitation: vi.fn(),
getInvitationByToken: vi.fn(),
acceptInvitation: vi.fn(),
}));
import {
useBusinesses,
usePlatformUsers,
useBusinessUsers,
useUpdateBusiness,
useCreateBusiness,
useDeleteBusiness,
useTenantInvitations,
useCreateTenantInvitation,
useResendTenantInvitation,
useCancelTenantInvitation,
useInvitationByToken,
useAcceptInvitation,
} from '../usePlatform';
import * as platformApi from '../../api/platform';
// Create wrapper
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('usePlatform hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
// ============================================================================
// Query Hooks
// ============================================================================
describe('useBusinesses', () => {
it('fetches all businesses successfully', async () => {
const mockBusinesses = [
{
id: 1,
name: 'Business 1',
subdomain: 'biz1',
tier: 'PROFESSIONAL',
is_active: true,
created_on: '2024-01-01',
user_count: 5,
owner: {
id: 10,
username: 'owner1',
full_name: 'Owner One',
email: 'owner1@test.com',
role: 'OWNER',
email_verified: true,
},
max_users: 10,
max_resources: 20,
can_manage_oauth_credentials: false,
can_accept_payments: true,
can_use_custom_domain: false,
can_white_label: false,
can_api_access: false,
},
{
id: 2,
name: 'Business 2',
subdomain: 'biz2',
tier: 'FREE',
is_active: true,
created_on: '2024-01-02',
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(platformApi.getBusinesses).mockResolvedValue(mockBusinesses);
const { result } = renderHook(() => useBusinesses(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(platformApi.getBusinesses).toHaveBeenCalledOnce();
expect(result.current.data).toEqual(mockBusinesses);
expect(result.current.data).toHaveLength(2);
});
it('handles fetch error', async () => {
const mockError = new Error('Failed to fetch businesses');
vi.mocked(platformApi.getBusinesses).mockRejectedValue(mockError);
const { result } = renderHook(() => useBusinesses(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBe(mockError);
});
it('uses correct query key', async () => {
vi.mocked(platformApi.getBusinesses).mockResolvedValue([]);
const { result } = renderHook(() => useBusinesses(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
// Query key should be ['platform', 'businesses']
expect(platformApi.getBusinesses).toHaveBeenCalled();
});
});
describe('usePlatformUsers', () => {
it('fetches all platform users successfully', async () => {
const mockUsers = [
{
id: 1,
email: 'user1@test.com',
username: 'user1',
name: 'User One',
role: 'OWNER',
is_active: true,
is_staff: false,
is_superuser: false,
email_verified: true,
business: 1,
business_name: 'Business 1',
business_subdomain: 'biz1',
date_joined: '2024-01-01',
last_login: '2024-01-05',
},
{
id: 2,
email: 'user2@test.com',
username: 'user2',
role: 'STAFF',
is_active: true,
is_staff: false,
is_superuser: false,
email_verified: false,
business: null,
date_joined: '2024-01-02',
},
];
vi.mocked(platformApi.getUsers).mockResolvedValue(mockUsers);
const { result } = renderHook(() => usePlatformUsers(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(platformApi.getUsers).toHaveBeenCalledOnce();
expect(result.current.data).toEqual(mockUsers);
expect(result.current.data).toHaveLength(2);
});
it('handles fetch error', async () => {
const mockError = new Error('Unauthorized');
vi.mocked(platformApi.getUsers).mockRejectedValue(mockError);
const { result } = renderHook(() => usePlatformUsers(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBe(mockError);
});
});
describe('useBusinessUsers', () => {
it('fetches users for specific business when businessId is provided', async () => {
const mockUsers = [
{
id: 1,
email: 'owner@business.com',
username: 'owner',
role: 'OWNER',
is_active: true,
is_staff: false,
is_superuser: false,
email_verified: true,
business: 5,
business_name: 'Test Business',
business_subdomain: 'test',
date_joined: '2024-01-01',
},
];
vi.mocked(platformApi.getBusinessUsers).mockResolvedValue(mockUsers);
const { result } = renderHook(() => useBusinessUsers(5), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(platformApi.getBusinessUsers).toHaveBeenCalledWith(5);
expect(result.current.data).toEqual(mockUsers);
});
it('does not fetch when businessId is null', async () => {
const { result } = renderHook(() => useBusinessUsers(null), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(platformApi.getBusinessUsers).not.toHaveBeenCalled();
expect(result.current.data).toBeUndefined();
});
it('does not fetch when businessId is 0', async () => {
const { result } = renderHook(() => useBusinessUsers(0), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(platformApi.getBusinessUsers).not.toHaveBeenCalled();
});
it('handles fetch error', async () => {
const mockError = new Error('Business not found');
vi.mocked(platformApi.getBusinessUsers).mockRejectedValue(mockError);
const { result } = renderHook(() => useBusinessUsers(999), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBe(mockError);
});
});
// ============================================================================
// Business Mutation Hooks
// ============================================================================
describe('useUpdateBusiness', () => {
it('updates business and invalidates cache', async () => {
const mockUpdatedBusiness = {
id: 1,
name: 'Updated Business',
subdomain: 'updated',
tier: 'ENTERPRISE',
is_active: true,
created_on: '2024-01-01',
user_count: 10,
owner: null,
max_users: 50,
max_resources: 100,
can_manage_oauth_credentials: true,
can_accept_payments: true,
can_use_custom_domain: true,
can_white_label: true,
can_api_access: true,
};
vi.mocked(platformApi.updateBusiness).mockResolvedValue(mockUpdatedBusiness);
const wrapper = createWrapper();
const { result } = renderHook(() => useUpdateBusiness(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
businessId: 1,
data: {
name: 'Updated Business',
subscription_tier: 'ENTERPRISE',
can_manage_oauth_credentials: true,
},
});
});
expect(platformApi.updateBusiness).toHaveBeenCalledWith(1, {
name: 'Updated Business',
subscription_tier: 'ENTERPRISE',
can_manage_oauth_credentials: true,
});
});
it('handles update error', async () => {
const mockError = new Error('Validation failed');
vi.mocked(platformApi.updateBusiness).mockRejectedValue(mockError);
const { result } = renderHook(() => useUpdateBusiness(), {
wrapper: createWrapper(),
});
let thrownError;
await act(async () => {
try {
await result.current.mutateAsync({
businessId: 1,
data: { name: '' },
});
} catch (error) {
thrownError = error;
}
});
expect(thrownError).toBe(mockError);
});
});
describe('useCreateBusiness', () => {
it('creates business and invalidates cache', async () => {
const mockNewBusiness = {
id: 3,
name: 'New Business',
subdomain: 'newbiz',
tier: 'STARTER',
is_active: true,
created_on: '2024-01-10',
user_count: 0,
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(platformApi.createBusiness).mockResolvedValue(mockNewBusiness);
const { result } = renderHook(() => useCreateBusiness(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
name: 'New Business',
subdomain: 'newbiz',
subscription_tier: 'STARTER',
});
});
expect(platformApi.createBusiness).toHaveBeenCalledWith({
name: 'New Business',
subdomain: 'newbiz',
subscription_tier: 'STARTER',
});
});
it('creates business with owner details', async () => {
const mockNewBusiness = {
id: 4,
name: 'Business with Owner',
subdomain: 'withowner',
tier: 'PROFESSIONAL',
is_active: true,
created_on: '2024-01-10',
user_count: 1,
owner: {
id: 20,
username: 'newowner',
full_name: 'New Owner',
email: 'owner@new.com',
role: 'OWNER',
email_verified: false,
},
max_users: 10,
max_resources: 20,
can_manage_oauth_credentials: false,
can_accept_payments: true,
can_use_custom_domain: false,
can_white_label: false,
can_api_access: false,
};
vi.mocked(platformApi.createBusiness).mockResolvedValue(mockNewBusiness);
const { result } = renderHook(() => useCreateBusiness(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
name: 'Business with Owner',
subdomain: 'withowner',
subscription_tier: 'PROFESSIONAL',
owner_email: 'owner@new.com',
owner_name: 'New Owner',
owner_password: 'securepass123',
});
});
expect(platformApi.createBusiness).toHaveBeenCalledWith({
name: 'Business with Owner',
subdomain: 'withowner',
subscription_tier: 'PROFESSIONAL',
owner_email: 'owner@new.com',
owner_name: 'New Owner',
owner_password: 'securepass123',
});
});
it('handles creation error', async () => {
const mockError = new Error('Subdomain already exists');
vi.mocked(platformApi.createBusiness).mockRejectedValue(mockError);
const { result } = renderHook(() => useCreateBusiness(), {
wrapper: createWrapper(),
});
let thrownError;
await act(async () => {
try {
await result.current.mutateAsync({
name: 'Duplicate',
subdomain: 'duplicate',
});
} catch (error) {
thrownError = error;
}
});
expect(thrownError).toBe(mockError);
});
});
describe('useDeleteBusiness', () => {
it('deletes business and invalidates cache', async () => {
vi.mocked(platformApi.deleteBusiness).mockResolvedValue(undefined);
const { result } = renderHook(() => useDeleteBusiness(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(5);
});
expect(platformApi.deleteBusiness).toHaveBeenCalledWith(5);
});
it('handles deletion error', async () => {
const mockError = new Error('Cannot delete business with active users');
vi.mocked(platformApi.deleteBusiness).mockRejectedValue(mockError);
const { result } = renderHook(() => useDeleteBusiness(), {
wrapper: createWrapper(),
});
let thrownError;
await act(async () => {
try {
await result.current.mutateAsync(1);
} catch (error) {
thrownError = error;
}
});
expect(thrownError).toBe(mockError);
});
});
// ============================================================================
// Tenant Invitation Query Hooks
// ============================================================================
describe('useTenantInvitations', () => {
it('fetches all tenant invitations successfully', async () => {
const mockInvitations = [
{
id: 1,
email: 'invited1@test.com',
token: 'token123',
status: 'PENDING' as const,
suggested_business_name: 'Suggested Biz 1',
subscription_tier: 'PROFESSIONAL' as const,
custom_max_users: null,
custom_max_resources: null,
permissions: {
can_accept_payments: true,
},
personal_message: 'Welcome!',
invited_by: 1,
invited_by_email: 'admin@platform.com',
created_at: '2024-01-01',
expires_at: '2024-01-08',
accepted_at: null,
created_tenant: null,
created_tenant_name: null,
created_user: null,
created_user_email: null,
},
{
id: 2,
email: 'accepted@test.com',
token: 'token456',
status: 'ACCEPTED' as const,
suggested_business_name: 'Accepted Biz',
subscription_tier: 'STARTER' as const,
custom_max_users: 10,
custom_max_resources: 20,
permissions: {},
personal_message: '',
invited_by: 1,
invited_by_email: 'admin@platform.com',
created_at: '2024-01-01',
expires_at: '2024-01-08',
accepted_at: '2024-01-02',
created_tenant: 5,
created_tenant_name: 'Accepted Business',
created_user: 10,
created_user_email: 'accepted@test.com',
},
];
vi.mocked(platformApi.getTenantInvitations).mockResolvedValue(mockInvitations);
const { result } = renderHook(() => useTenantInvitations(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(platformApi.getTenantInvitations).toHaveBeenCalledOnce();
expect(result.current.data).toEqual(mockInvitations);
expect(result.current.data).toHaveLength(2);
});
it('handles fetch error', async () => {
const mockError = new Error('Failed to fetch invitations');
vi.mocked(platformApi.getTenantInvitations).mockRejectedValue(mockError);
const { result } = renderHook(() => useTenantInvitations(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBe(mockError);
});
});
describe('useInvitationByToken', () => {
it('fetches invitation details when token is provided', async () => {
const mockInvitation = {
email: 'invited@test.com',
suggested_business_name: 'Test Business',
subscription_tier: 'PROFESSIONAL',
effective_max_users: 10,
effective_max_resources: 20,
permissions: {
can_accept_payments: true,
can_manage_oauth_credentials: false,
},
expires_at: '2024-12-31',
};
vi.mocked(platformApi.getInvitationByToken).mockResolvedValue(mockInvitation);
const { result } = renderHook(() => useInvitationByToken('valid-token-123'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(platformApi.getInvitationByToken).toHaveBeenCalledWith('valid-token-123');
expect(result.current.data).toEqual(mockInvitation);
});
it('does not fetch when token is null', async () => {
const { result } = renderHook(() => useInvitationByToken(null), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(platformApi.getInvitationByToken).not.toHaveBeenCalled();
expect(result.current.data).toBeUndefined();
});
it('does not fetch when token is empty string', async () => {
const { result } = renderHook(() => useInvitationByToken(''), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(platformApi.getInvitationByToken).not.toHaveBeenCalled();
});
it('does not retry on error (expired/invalid token)', async () => {
const mockError = new Error('Invitation expired');
vi.mocked(platformApi.getInvitationByToken).mockRejectedValue(mockError);
const { result } = renderHook(() => useInvitationByToken('expired-token'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
// Should only be called once (no retries)
expect(platformApi.getInvitationByToken).toHaveBeenCalledTimes(1);
expect(result.current.error).toBe(mockError);
});
});
// ============================================================================
// Tenant Invitation Mutation Hooks
// ============================================================================
describe('useCreateTenantInvitation', () => {
it('creates invitation and invalidates cache', async () => {
const mockInvitation = {
id: 10,
email: 'new@invite.com',
token: 'new-token',
status: 'PENDING' as const,
suggested_business_name: 'New Business',
subscription_tier: 'STARTER' as const,
custom_max_users: null,
custom_max_resources: null,
permissions: {},
personal_message: 'Join us!',
invited_by: 1,
invited_by_email: 'admin@platform.com',
created_at: '2024-01-10',
expires_at: '2024-01-17',
accepted_at: null,
created_tenant: null,
created_tenant_name: null,
created_user: null,
created_user_email: null,
};
vi.mocked(platformApi.createTenantInvitation).mockResolvedValue(mockInvitation);
const { result } = renderHook(() => useCreateTenantInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
email: 'new@invite.com',
subscription_tier: 'STARTER',
suggested_business_name: 'New Business',
personal_message: 'Join us!',
});
});
expect(platformApi.createTenantInvitation).toHaveBeenCalledWith({
email: 'new@invite.com',
subscription_tier: 'STARTER',
suggested_business_name: 'New Business',
personal_message: 'Join us!',
});
});
it('creates invitation with custom limits and permissions', async () => {
const mockInvitation = {
id: 11,
email: 'custom@invite.com',
token: 'custom-token',
status: 'PENDING' as const,
suggested_business_name: 'Custom Biz',
subscription_tier: 'ENTERPRISE' as const,
custom_max_users: 100,
custom_max_resources: 200,
permissions: {
can_manage_oauth_credentials: true,
can_accept_payments: true,
can_use_custom_domain: true,
},
personal_message: '',
invited_by: 1,
invited_by_email: 'admin@platform.com',
created_at: '2024-01-10',
expires_at: '2024-01-17',
accepted_at: null,
created_tenant: null,
created_tenant_name: null,
created_user: null,
created_user_email: null,
};
vi.mocked(platformApi.createTenantInvitation).mockResolvedValue(mockInvitation);
const { result } = renderHook(() => useCreateTenantInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
email: 'custom@invite.com',
subscription_tier: 'ENTERPRISE',
custom_max_users: 100,
custom_max_resources: 200,
permissions: {
can_manage_oauth_credentials: true,
can_accept_payments: true,
can_use_custom_domain: true,
},
});
});
expect(platformApi.createTenantInvitation).toHaveBeenCalledWith({
email: 'custom@invite.com',
subscription_tier: 'ENTERPRISE',
custom_max_users: 100,
custom_max_resources: 200,
permissions: {
can_manage_oauth_credentials: true,
can_accept_payments: true,
can_use_custom_domain: true,
},
});
});
it('handles creation error', async () => {
const mockError = new Error('Email already invited');
vi.mocked(platformApi.createTenantInvitation).mockRejectedValue(mockError);
const { result } = renderHook(() => useCreateTenantInvitation(), {
wrapper: createWrapper(),
});
let thrownError;
await act(async () => {
try {
await result.current.mutateAsync({
email: 'duplicate@test.com',
subscription_tier: 'FREE',
});
} catch (error) {
thrownError = error;
}
});
expect(thrownError).toBe(mockError);
});
});
describe('useResendTenantInvitation', () => {
it('resends invitation and invalidates cache', async () => {
vi.mocked(platformApi.resendTenantInvitation).mockResolvedValue(undefined);
const { result } = renderHook(() => useResendTenantInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(5);
});
expect(platformApi.resendTenantInvitation).toHaveBeenCalledWith(5);
});
it('handles resend error', async () => {
const mockError = new Error('Invitation already accepted');
vi.mocked(platformApi.resendTenantInvitation).mockRejectedValue(mockError);
const { result } = renderHook(() => useResendTenantInvitation(), {
wrapper: createWrapper(),
});
let thrownError;
await act(async () => {
try {
await result.current.mutateAsync(10);
} catch (error) {
thrownError = error;
}
});
expect(thrownError).toBe(mockError);
});
});
describe('useCancelTenantInvitation', () => {
it('cancels invitation and invalidates cache', async () => {
vi.mocked(platformApi.cancelTenantInvitation).mockResolvedValue(undefined);
const { result } = renderHook(() => useCancelTenantInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(3);
});
expect(platformApi.cancelTenantInvitation).toHaveBeenCalledWith(3);
});
it('handles cancel error', async () => {
const mockError = new Error('Invitation not found');
vi.mocked(platformApi.cancelTenantInvitation).mockRejectedValue(mockError);
const { result } = renderHook(() => useCancelTenantInvitation(), {
wrapper: createWrapper(),
});
let thrownError;
await act(async () => {
try {
await result.current.mutateAsync(999);
} catch (error) {
thrownError = error;
}
});
expect(thrownError).toBe(mockError);
});
});
describe('useAcceptInvitation', () => {
it('accepts invitation successfully', async () => {
const mockResponse = {
detail: 'Invitation accepted successfully',
};
vi.mocked(platformApi.acceptInvitation).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
token: 'valid-token',
data: {
email: 'user@test.com',
password: 'securepass123',
first_name: 'John',
last_name: 'Doe',
business_name: 'My Business',
subdomain: 'mybiz',
contact_email: 'contact@mybiz.com',
phone: '+1234567890',
},
});
});
expect(platformApi.acceptInvitation).toHaveBeenCalledWith('valid-token', {
email: 'user@test.com',
password: 'securepass123',
first_name: 'John',
last_name: 'Doe',
business_name: 'My Business',
subdomain: 'mybiz',
contact_email: 'contact@mybiz.com',
phone: '+1234567890',
});
});
it('accepts invitation with minimal data', async () => {
const mockResponse = {
detail: 'Invitation accepted',
};
vi.mocked(platformApi.acceptInvitation).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
token: 'minimal-token',
data: {
email: 'user@test.com',
password: 'pass123',
first_name: 'Jane',
last_name: 'Smith',
business_name: 'Jane Biz',
subdomain: 'janebiz',
},
});
});
expect(platformApi.acceptInvitation).toHaveBeenCalledWith('minimal-token', {
email: 'user@test.com',
password: 'pass123',
first_name: 'Jane',
last_name: 'Smith',
business_name: 'Jane Biz',
subdomain: 'janebiz',
});
});
it('handles acceptance error', async () => {
const mockError = new Error('Subdomain already taken');
vi.mocked(platformApi.acceptInvitation).mockRejectedValue(mockError);
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
let thrownError;
await act(async () => {
try {
await result.current.mutateAsync({
token: 'token',
data: {
email: 'user@test.com',
password: 'pass',
first_name: 'Test',
last_name: 'User',
business_name: 'Test',
subdomain: 'taken',
},
});
} catch (error) {
thrownError = error;
}
});
expect(thrownError).toBe(mockError);
});
});
// ============================================================================
// Cache Invalidation Tests
// ============================================================================
describe('Cache invalidation', () => {
it('useUpdateBusiness invalidates businesses query', async () => {
vi.mocked(platformApi.updateBusiness).mockResolvedValue({
id: 1,
name: 'Updated',
subdomain: 'updated',
tier: 'FREE',
is_active: true,
created_on: '2024-01-01',
user_count: 0,
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,
});
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const spy = vi.spyOn(queryClient, 'invalidateQueries');
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useUpdateBusiness(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
businessId: 1,
data: { name: 'Updated' },
});
});
expect(spy).toHaveBeenCalledWith({ queryKey: ['platform', 'businesses'] });
});
it('useCreateBusiness invalidates businesses query', async () => {
vi.mocked(platformApi.createBusiness).mockResolvedValue({
id: 1,
name: 'New',
subdomain: 'new',
tier: 'FREE',
is_active: true,
created_on: '2024-01-01',
user_count: 0,
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,
});
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const spy = vi.spyOn(queryClient, 'invalidateQueries');
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useCreateBusiness(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
name: 'New',
subdomain: 'new',
});
});
expect(spy).toHaveBeenCalledWith({ queryKey: ['platform', 'businesses'] });
});
it('useDeleteBusiness invalidates businesses query', async () => {
vi.mocked(platformApi.deleteBusiness).mockResolvedValue(undefined);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const spy = vi.spyOn(queryClient, 'invalidateQueries');
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useDeleteBusiness(), { wrapper });
await act(async () => {
await result.current.mutateAsync(1);
});
expect(spy).toHaveBeenCalledWith({ queryKey: ['platform', 'businesses'] });
});
it('useCreateTenantInvitation invalidates invitations query', async () => {
vi.mocked(platformApi.createTenantInvitation).mockResolvedValue({
id: 1,
email: 'test@test.com',
token: 'token',
status: 'PENDING',
suggested_business_name: 'Test',
subscription_tier: 'FREE',
custom_max_users: null,
custom_max_resources: null,
permissions: {},
personal_message: '',
invited_by: 1,
invited_by_email: 'admin@test.com',
created_at: '2024-01-01',
expires_at: '2024-01-08',
accepted_at: null,
created_tenant: null,
created_tenant_name: null,
created_user: null,
created_user_email: null,
});
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const spy = vi.spyOn(queryClient, 'invalidateQueries');
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useCreateTenantInvitation(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
email: 'test@test.com',
subscription_tier: 'FREE',
});
});
expect(spy).toHaveBeenCalledWith({ queryKey: ['platform', 'tenant-invitations'] });
});
it('useResendTenantInvitation invalidates invitations query', async () => {
vi.mocked(platformApi.resendTenantInvitation).mockResolvedValue(undefined);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const spy = vi.spyOn(queryClient, 'invalidateQueries');
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useResendTenantInvitation(), { wrapper });
await act(async () => {
await result.current.mutateAsync(1);
});
expect(spy).toHaveBeenCalledWith({ queryKey: ['platform', 'tenant-invitations'] });
});
it('useCancelTenantInvitation invalidates invitations query', async () => {
vi.mocked(platformApi.cancelTenantInvitation).mockResolvedValue(undefined);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const spy = vi.spyOn(queryClient, 'invalidateQueries');
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useCancelTenantInvitation(), { wrapper });
await act(async () => {
await result.current.mutateAsync(1);
});
expect(spy).toHaveBeenCalledWith({ queryKey: ['platform', 'tenant-invitations'] });
});
});
});