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'] }); }); }); });