Files
smoothschedule/frontend/src/hooks/__tests__/usePlatformSettings.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

1025 lines
31 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 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);
});
});
});
});