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 apiClient vi.mock('../../api/client', () => ({ default: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn(), }, })); import { usePlatformSettings, useUpdateGeneralSettings, useUpdateStripeKeys, useValidateStripeKeys, useSubscriptionPlans, useCreateSubscriptionPlan, useUpdateSubscriptionPlan, useDeleteSubscriptionPlan, useSyncPlansWithStripe, useSyncPlanToTenants, } from '../usePlatformSettings'; import apiClient from '../../api/client'; // 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('usePlatformSettings hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('usePlatformSettings', () => { it('fetches platform settings successfully', async () => { const mockSettings = { stripe_secret_key_masked: 'sk_test_***', stripe_publishable_key_masked: 'pk_test_***', stripe_webhook_secret_masked: 'whsec_***', stripe_account_id: 'acct_123', stripe_account_name: 'Test Account', stripe_keys_validated_at: '2025-12-07T10:00:00Z', stripe_validation_error: '', has_stripe_keys: true, stripe_keys_from_env: false, email_check_interval_minutes: 5, updated_at: '2025-12-07T10:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockSettings }); const { result } = renderHook(() => usePlatformSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/platform/settings/'); expect(result.current.data).toEqual(mockSettings); }); it('handles fetch error', async () => { vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error')); const { result } = renderHook(() => usePlatformSettings(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toBeInstanceOf(Error); }); it('uses staleTime of 5 minutes', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: {} }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => usePlatformSettings(), { wrapper }); await waitFor(() => { expect(apiClient.get).toHaveBeenCalled(); }); // Query should be cached with 5 minute stale time const cachedQuery = queryClient.getQueryState(['platformSettings']); expect(cachedQuery).toBeDefined(); }); }); describe('useUpdateGeneralSettings', () => { it('updates general settings successfully', async () => { const updatedSettings = { email_check_interval_minutes: 10, updated_at: '2025-12-07T11:00:00Z', }; vi.mocked(apiClient.post).mockResolvedValue({ data: updatedSettings }); const { result } = renderHook(() => useUpdateGeneralSettings(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ email_check_interval_minutes: 10, }); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/settings/general/', { email_check_interval_minutes: 10, }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('updates query cache on success', async () => { const updatedSettings = { stripe_secret_key_masked: 'sk_test_***', stripe_publishable_key_masked: 'pk_test_***', stripe_webhook_secret_masked: 'whsec_***', stripe_account_id: 'acct_123', stripe_account_name: 'Test Account', stripe_keys_validated_at: null, stripe_validation_error: '', has_stripe_keys: true, stripe_keys_from_env: false, email_check_interval_minutes: 15, updated_at: '2025-12-07T11:00:00Z', }; vi.mocked(apiClient.post).mockResolvedValue({ data: updatedSettings }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateGeneralSettings(), { wrapper }); await act(async () => { await result.current.mutateAsync({ email_check_interval_minutes: 15, }); }); const cachedData = queryClient.getQueryData(['platformSettings']); expect(cachedData).toEqual(updatedSettings); }); it('handles update error', async () => { vi.mocked(apiClient.post).mockRejectedValue(new Error('Update failed')); const { result } = renderHook(() => useUpdateGeneralSettings(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync({ email_check_interval_minutes: 5 }); } catch (error) { expect(error).toBeInstanceOf(Error); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); describe('useUpdateStripeKeys', () => { it('updates Stripe keys successfully', async () => { const updatedSettings = { stripe_secret_key_masked: 'sk_live_***', stripe_publishable_key_masked: 'pk_live_***', stripe_webhook_secret_masked: 'whsec_***', has_stripe_keys: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: updatedSettings }); const { result } = renderHook(() => useUpdateStripeKeys(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ stripe_secret_key: 'sk_live_newkey', stripe_publishable_key: 'pk_live_newkey', stripe_webhook_secret: 'whsec_newsecret', }); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/settings/stripe/keys/', { stripe_secret_key: 'sk_live_newkey', stripe_publishable_key: 'pk_live_newkey', stripe_webhook_secret: 'whsec_newsecret', }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('updates partial Stripe keys', async () => { const updatedSettings = { stripe_secret_key_masked: 'sk_live_***', has_stripe_keys: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: updatedSettings }); const { result } = renderHook(() => useUpdateStripeKeys(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ stripe_secret_key: 'sk_live_newkey', }); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/settings/stripe/keys/', { stripe_secret_key: 'sk_live_newkey', }); }); it('updates query cache on success', async () => { const updatedSettings = { stripe_secret_key_masked: 'sk_live_***', has_stripe_keys: true, }; vi.mocked(apiClient.post).mockResolvedValue({ data: updatedSettings }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateStripeKeys(), { wrapper }); await act(async () => { await result.current.mutateAsync({ stripe_secret_key: 'sk_live_newkey', }); }); const cachedData = queryClient.getQueryData(['platformSettings']); expect(cachedData).toEqual(updatedSettings); }); }); describe('useValidateStripeKeys', () => { it('validates Stripe keys successfully', async () => { const responseData = { success: true, message: 'Stripe keys validated successfully', settings: { stripe_keys_validated_at: '2025-12-07T12:00:00Z', stripe_validation_error: '', stripe_account_id: 'acct_123', stripe_account_name: 'Test Account', }, }; vi.mocked(apiClient.post).mockResolvedValue({ data: responseData }); const { result } = renderHook(() => useValidateStripeKeys(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/settings/stripe/validate/'); await waitFor(() => { expect(result.current.data).toEqual(responseData); }); }); it('updates query cache with settings on success', async () => { const updatedSettings = { stripe_keys_validated_at: '2025-12-07T12:00:00Z', stripe_validation_error: '', stripe_account_id: 'acct_123', stripe_account_name: 'Test Account', }; const responseData = { success: true, message: 'Validated', settings: updatedSettings, }; vi.mocked(apiClient.post).mockResolvedValue({ data: responseData }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useValidateStripeKeys(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); const cachedData = queryClient.getQueryData(['platformSettings']); expect(cachedData).toEqual(updatedSettings); }); it('does not update cache when settings is not in response', async () => { const responseData = { success: false, message: 'Invalid keys', }; vi.mocked(apiClient.post).mockResolvedValue({ data: responseData }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useValidateStripeKeys(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); const cachedData = queryClient.getQueryData(['platformSettings']); expect(cachedData).toBeUndefined(); }); it('handles validation error', async () => { vi.mocked(apiClient.post).mockRejectedValue(new Error('Validation failed')); const { result } = renderHook(() => useValidateStripeKeys(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync(); } catch (error) { expect(error).toBeInstanceOf(Error); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); describe('useSubscriptionPlans', () => { it('fetches subscription plans successfully', async () => { const mockPlans = [ { id: 1, name: 'Basic', description: 'Basic plan', plan_type: 'base' as const, stripe_product_id: 'prod_123', stripe_price_id: 'price_123', price_monthly: '29.99', price_yearly: '299.99', business_tier: 'basic', features: ['feature1', 'feature2'], limits: { max_users: 10 }, permissions: { can_use_plugins: false }, transaction_fee_percent: '2.5', transaction_fee_fixed: '0.30', sms_enabled: true, sms_price_per_message_cents: 5, masked_calling_enabled: false, masked_calling_price_per_minute_cents: 0, proxy_number_enabled: false, proxy_number_monthly_fee_cents: 0, contracts_enabled: false, default_auto_reload_enabled: true, default_auto_reload_threshold_cents: 500, default_auto_reload_amount_cents: 2000, is_active: true, is_public: true, is_most_popular: false, show_price: true, created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }, { id: 2, name: 'Pro', description: 'Pro plan', plan_type: 'base' as const, stripe_product_id: 'prod_456', stripe_price_id: 'price_456', price_monthly: '99.99', price_yearly: null, business_tier: 'pro', features: ['feature1', 'feature2', 'feature3'], limits: { max_users: 50 }, permissions: { can_use_plugins: true }, transaction_fee_percent: '2.0', transaction_fee_fixed: '0.30', sms_enabled: true, sms_price_per_message_cents: 4, masked_calling_enabled: true, masked_calling_price_per_minute_cents: 10, proxy_number_enabled: true, proxy_number_monthly_fee_cents: 1500, contracts_enabled: true, default_auto_reload_enabled: false, default_auto_reload_threshold_cents: 0, default_auto_reload_amount_cents: 0, is_active: true, is_public: true, is_most_popular: true, show_price: true, created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlans }); const { result } = renderHook(() => useSubscriptionPlans(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/platform/subscription-plans/'); expect(result.current.data).toEqual(mockPlans); expect(result.current.data).toHaveLength(2); }); it('handles fetch error', async () => { vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error')); const { result } = renderHook(() => useSubscriptionPlans(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toBeInstanceOf(Error); }); it('uses staleTime of 5 minutes', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useSubscriptionPlans(), { wrapper }); await waitFor(() => { expect(apiClient.get).toHaveBeenCalled(); }); const cachedQuery = queryClient.getQueryState(['subscriptionPlans']); expect(cachedQuery).toBeDefined(); }); }); describe('useCreateSubscriptionPlan', () => { it('creates subscription plan successfully', async () => { const newPlan = { name: 'Enterprise', description: 'Enterprise plan', plan_type: 'base' as const, price_monthly: 199.99, price_yearly: 1999.99, business_tier: 'enterprise', features: ['all_features'], limits: { max_users: 100 }, permissions: { can_use_plugins: true }, transaction_fee_percent: 1.5, transaction_fee_fixed: 0.30, create_stripe_product: true, }; const createdPlan = { id: 3, ...newPlan, price_monthly: '199.99', price_yearly: '1999.99', transaction_fee_percent: '1.5', transaction_fee_fixed: '0.30', stripe_product_id: 'prod_789', stripe_price_id: 'price_789', sms_enabled: false, sms_price_per_message_cents: 0, masked_calling_enabled: false, masked_calling_price_per_minute_cents: 0, proxy_number_enabled: false, proxy_number_monthly_fee_cents: 0, contracts_enabled: false, default_auto_reload_enabled: false, default_auto_reload_threshold_cents: 0, default_auto_reload_amount_cents: 0, is_active: true, is_public: true, is_most_popular: false, show_price: true, created_at: '2025-12-07T12:00:00Z', updated_at: '2025-12-07T12:00:00Z', }; vi.mocked(apiClient.post).mockResolvedValue({ data: createdPlan }); const { result } = renderHook(() => useCreateSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(newPlan); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/subscription-plans/', newPlan); await waitFor(() => { expect(result.current.isSuccess).toBe(true); expect(result.current.data).toEqual(createdPlan); }); }); it('creates plan with minimal data', async () => { const minimalPlan = { name: 'Minimal Plan', }; const createdPlan = { id: 4, name: 'Minimal Plan', description: '', plan_type: 'base' as const, stripe_product_id: '', stripe_price_id: '', price_monthly: null, price_yearly: null, business_tier: '', features: [], limits: {}, permissions: {}, transaction_fee_percent: '0', transaction_fee_fixed: '0', sms_enabled: false, sms_price_per_message_cents: 0, masked_calling_enabled: false, masked_calling_price_per_minute_cents: 0, proxy_number_enabled: false, proxy_number_monthly_fee_cents: 0, contracts_enabled: false, default_auto_reload_enabled: false, default_auto_reload_threshold_cents: 0, default_auto_reload_amount_cents: 0, is_active: true, is_public: false, is_most_popular: false, show_price: false, created_at: '2025-12-07T12:00:00Z', updated_at: '2025-12-07T12:00:00Z', }; vi.mocked(apiClient.post).mockResolvedValue({ data: createdPlan }); const { result } = renderHook(() => useCreateSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(minimalPlan); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/subscription-plans/', minimalPlan); }); it('invalidates subscription plans query on success', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 5 } }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); // Spy on invalidateQueries const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useCreateSubscriptionPlan(), { wrapper }); await act(async () => { await result.current.mutateAsync({ name: 'Test Plan' }); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['subscriptionPlans'] }); }); it('handles create error', async () => { vi.mocked(apiClient.post).mockRejectedValue(new Error('Create failed')); const { result } = renderHook(() => useCreateSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync({ name: 'Test' }); } catch (error) { expect(error).toBeInstanceOf(Error); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); describe('useUpdateSubscriptionPlan', () => { it('updates subscription plan successfully', async () => { const updates = { id: 1, name: 'Updated Basic', price_monthly: 39.99, is_most_popular: true, }; const updatedPlan = { id: 1, name: 'Updated Basic', price_monthly: '39.99', is_most_popular: true, description: 'Basic plan', plan_type: 'base' as const, stripe_product_id: 'prod_123', stripe_price_id: 'price_123', price_yearly: '299.99', business_tier: 'basic', features: ['feature1', 'feature2'], limits: { max_users: 10 }, permissions: { can_use_plugins: false }, transaction_fee_percent: '2.5', transaction_fee_fixed: '0.30', sms_enabled: true, sms_price_per_message_cents: 5, masked_calling_enabled: false, masked_calling_price_per_minute_cents: 0, proxy_number_enabled: false, proxy_number_monthly_fee_cents: 0, contracts_enabled: false, default_auto_reload_enabled: true, default_auto_reload_threshold_cents: 500, default_auto_reload_amount_cents: 2000, is_active: true, is_public: true, show_price: true, created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T13:00:00Z', }; vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedPlan }); const { result } = renderHook(() => useUpdateSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(updates); }); expect(apiClient.patch).toHaveBeenCalledWith('/platform/subscription-plans/1/', { name: 'Updated Basic', price_monthly: 39.99, is_most_popular: true, }); await waitFor(() => { expect(result.current.data).toEqual(updatedPlan); }); }); it('updates only specified fields', async () => { const updates = { id: 2, is_active: false, }; vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 2, is_active: false } }); const { result } = renderHook(() => useUpdateSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(updates); }); expect(apiClient.patch).toHaveBeenCalledWith('/platform/subscription-plans/2/', { is_active: false, }); }); it('invalidates subscription plans query on success', async () => { vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateSubscriptionPlan(), { wrapper }); await act(async () => { await result.current.mutateAsync({ id: 1, name: 'Updated' }); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['subscriptionPlans'] }); }); it('handles update error', async () => { vi.mocked(apiClient.patch).mockRejectedValue(new Error('Update failed')); const { result } = renderHook(() => useUpdateSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync({ id: 1, name: 'Fail' }); } catch (error) { expect(error).toBeInstanceOf(Error); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); describe('useDeleteSubscriptionPlan', () => { it('deletes subscription plan successfully', async () => { vi.mocked(apiClient.delete).mockResolvedValue({ data: { success: true } }); const { result } = renderHook(() => useDeleteSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(3); }); expect(apiClient.delete).toHaveBeenCalledWith('/platform/subscription-plans/3/'); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); }); it('invalidates subscription plans query on success', async () => { vi.mocked(apiClient.delete).mockResolvedValue({}); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useDeleteSubscriptionPlan(), { wrapper }); await act(async () => { await result.current.mutateAsync(5); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['subscriptionPlans'] }); }); it('handles delete error', async () => { vi.mocked(apiClient.delete).mockRejectedValue(new Error('Delete failed')); const { result } = renderHook(() => useDeleteSubscriptionPlan(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync(1); } catch (error) { expect(error).toBeInstanceOf(Error); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); describe('useSyncPlansWithStripe', () => { it('syncs plans with Stripe successfully', async () => { const syncResponse = { success: true, message: 'Plans synced successfully', synced_count: 3, }; vi.mocked(apiClient.post).mockResolvedValue({ data: syncResponse }); const { result } = renderHook(() => useSyncPlansWithStripe(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/subscription-plans/sync_with_stripe/'); await waitFor(() => { expect(result.current.data).toEqual(syncResponse); }); }); it('invalidates subscription plans query on success', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: { success: true } }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useSyncPlansWithStripe(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['subscriptionPlans'] }); }); it('handles sync error', async () => { vi.mocked(apiClient.post).mockRejectedValue(new Error('Sync failed')); const { result } = renderHook(() => useSyncPlansWithStripe(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync(); } catch (error) { expect(error).toBeInstanceOf(Error); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); describe('useSyncPlanToTenants', () => { it('syncs plan to tenants successfully', async () => { const syncResponse = { message: 'Plan synced to 15 tenants', tenant_count: 15, }; vi.mocked(apiClient.post).mockResolvedValue({ data: syncResponse }); const { result } = renderHook(() => useSyncPlanToTenants(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(1); }); expect(apiClient.post).toHaveBeenCalledWith('/platform/subscription-plans/1/sync_tenants/'); await waitFor(() => { expect(result.current.data).toEqual(syncResponse); }); }); it('handles zero tenants', async () => { const syncResponse = { message: 'No tenants on this plan', tenant_count: 0, }; vi.mocked(apiClient.post).mockResolvedValue({ data: syncResponse }); const { result } = renderHook(() => useSyncPlanToTenants(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync(5); }); await waitFor(() => { expect(result.current.data?.tenant_count).toBe(0); }); }); it('does not invalidate queries (no onSuccess callback)', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: { message: 'Done', tenant_count: 5 } }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useSyncPlanToTenants(), { wrapper }); await act(async () => { await result.current.mutateAsync(2); }); // This mutation does not have an onSuccess callback, so it shouldn't invalidate expect(invalidateQueriesSpy).not.toHaveBeenCalled(); }); it('handles sync error', async () => { vi.mocked(apiClient.post).mockRejectedValue(new Error('Sync to tenants failed')); const { result } = renderHook(() => useSyncPlanToTenants(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync(1); } catch (error) { expect(error).toBeInstanceOf(Error); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); }); });