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

943 lines
30 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import apiClient from '../../api/client';
import {
useCommunicationCredits,
useCreditTransactions,
useUpdateCreditsSettings,
useAddCredits,
useCreatePaymentIntent,
useConfirmPayment,
useSetupPaymentMethod,
useSavePaymentMethod,
useCommunicationUsageStats,
usePhoneNumbers,
useSearchPhoneNumbers,
usePurchasePhoneNumber,
useReleasePhoneNumber,
useChangePhoneNumber,
CommunicationCredits,
CreditTransaction,
ProxyPhoneNumber,
AvailablePhoneNumber,
} from '../useCommunicationCredits';
// Mock the API client
vi.mock('../../api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
},
}));
describe('useCommunicationCredits', () => {
let queryClient: QueryClient;
let wrapper: React.FC<{ children: React.ReactNode }>;
const mockCredits: CommunicationCredits = {
id: 1,
balance_cents: 50000,
auto_reload_enabled: true,
auto_reload_threshold_cents: 10000,
auto_reload_amount_cents: 50000,
low_balance_warning_cents: 20000,
low_balance_warning_sent: false,
stripe_payment_method_id: 'pm_test123',
last_twilio_sync_at: '2025-12-07T10:00:00Z',
total_loaded_cents: 100000,
total_spent_cents: 50000,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-12-07T10:00:00Z',
};
const mockTransactions: CreditTransaction[] = [
{
id: 1,
amount_cents: 50000,
balance_after_cents: 50000,
transaction_type: 'manual',
description: 'Manual credit purchase',
reference_type: 'payment_intent',
reference_id: 'pi_test123',
stripe_charge_id: 'ch_test123',
created_at: '2025-12-07T10:00:00Z',
},
{
id: 2,
amount_cents: -1000,
balance_after_cents: 49000,
transaction_type: 'usage',
description: 'SMS to +15551234567',
reference_type: 'sms_message',
reference_id: 'msg_123',
stripe_charge_id: '',
created_at: '2025-12-07T11:00:00Z',
},
];
const mockPhoneNumber: ProxyPhoneNumber = {
id: 1,
phone_number: '+15551234567',
friendly_name: 'Main Office Line',
status: 'assigned',
monthly_fee_cents: 100,
capabilities: {
voice: true,
sms: true,
mms: true,
},
assigned_at: '2025-12-01T00:00:00Z',
last_billed_at: '2025-12-01T00:00:00Z',
};
const mockAvailableNumber: AvailablePhoneNumber = {
phone_number: '+15559876543',
friendly_name: '(555) 987-6543',
locality: 'New York',
region: 'NY',
postal_code: '10001',
capabilities: {
voice: true,
sms: true,
mms: true,
},
monthly_cost_cents: 100,
};
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
wrapper = ({ children }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
vi.clearAllMocks();
});
afterEach(() => {
queryClient.clear();
});
describe('useCommunicationCredits', () => {
it('should fetch communication credits successfully', async () => {
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockCredits });
const { result } = renderHook(() => useCommunicationCredits(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.get).toHaveBeenCalledWith('/communication-credits/');
expect(result.current.data).toEqual(mockCredits);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
it('should handle fetch errors', async () => {
const mockError = new Error('Failed to fetch credits');
vi.mocked(apiClient.get).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useCommunicationCredits(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.data).toBeUndefined();
expect(result.current.error).toEqual(mockError);
});
it('should use correct query key', async () => {
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockCredits });
const { result } = renderHook(() => useCommunicationCredits(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
const cachedData = queryClient.getQueryData(['communicationCredits']);
expect(cachedData).toEqual(mockCredits);
});
it('should have staleTime of 30 seconds', async () => {
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockCredits });
const { result } = renderHook(() => useCommunicationCredits(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
const queryState = queryClient.getQueryState(['communicationCredits']);
expect(queryState?.dataUpdatedAt).toBeDefined();
});
});
describe('useCreditTransactions', () => {
it('should fetch credit transactions with pagination', async () => {
const mockResponse = {
results: mockTransactions,
count: 2,
next: null,
previous: null,
};
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useCreditTransactions(1, 20), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.get).toHaveBeenCalledWith('/communication-credits/transactions/', {
params: { page: 1, limit: 20 },
});
expect(result.current.data).toEqual(mockResponse);
});
it('should support custom page and limit', async () => {
const mockResponse = {
results: [mockTransactions[0]],
count: 10,
next: 'http://api.example.com/page=3',
previous: 'http://api.example.com/page=1',
};
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useCreditTransactions(2, 10), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.get).toHaveBeenCalledWith('/communication-credits/transactions/', {
params: { page: 2, limit: 10 },
});
});
it('should handle fetch errors', async () => {
const mockError = new Error('Failed to fetch transactions');
vi.mocked(apiClient.get).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useCreditTransactions(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useUpdateCreditsSettings', () => {
it('should update credit settings successfully', async () => {
const updatedCredits = {
...mockCredits,
auto_reload_enabled: false,
auto_reload_threshold_cents: 5000,
};
vi.mocked(apiClient.patch).mockResolvedValueOnce({ data: updatedCredits });
const { result } = renderHook(() => useUpdateCreditsSettings(), { wrapper });
result.current.mutate({
auto_reload_enabled: false,
auto_reload_threshold_cents: 5000,
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.patch).toHaveBeenCalledWith('/communication-credits/settings/', {
auto_reload_enabled: false,
auto_reload_threshold_cents: 5000,
});
expect(result.current.data).toEqual(updatedCredits);
});
it('should update query cache on success', async () => {
const updatedCredits = { ...mockCredits, auto_reload_enabled: false };
queryClient.setQueryData(['communicationCredits'], mockCredits);
vi.mocked(apiClient.patch).mockResolvedValueOnce({ data: updatedCredits });
const { result } = renderHook(() => useUpdateCreditsSettings(), { wrapper });
result.current.mutate({ auto_reload_enabled: false });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
const cachedData = queryClient.getQueryData(['communicationCredits']);
expect(cachedData).toEqual(updatedCredits);
});
it('should handle update errors', async () => {
const mockError = new Error('Failed to update settings');
vi.mocked(apiClient.patch).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useUpdateCreditsSettings(), { wrapper });
result.current.mutate({ auto_reload_enabled: false });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useAddCredits', () => {
it('should add credits successfully', async () => {
const mockResponse = {
success: true,
balance_cents: 100000,
transaction_id: 123,
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useAddCredits(), { wrapper });
result.current.mutate({
amount_cents: 50000,
payment_method_id: 'pm_test123',
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/add/', {
amount_cents: 50000,
payment_method_id: 'pm_test123',
});
expect(result.current.data).toEqual(mockResponse);
});
it('should invalidate credits and transactions queries on success', async () => {
const mockResponse = { success: true, balance_cents: 100000 };
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const { result } = renderHook(() => useAddCredits(), { wrapper });
result.current.mutate({ amount_cents: 50000 });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['communicationCredits'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['creditTransactions'] });
});
it('should handle add credits errors', async () => {
const mockError = new Error('Payment failed');
vi.mocked(apiClient.post).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useAddCredits(), { wrapper });
result.current.mutate({ amount_cents: 50000 });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useCreatePaymentIntent', () => {
it('should create payment intent successfully', async () => {
const mockResponse = {
client_secret: 'pi_test_secret',
payment_intent_id: 'pi_test123',
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useCreatePaymentIntent(), { wrapper });
result.current.mutate(50000);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/create-payment-intent/', {
amount_cents: 50000,
});
expect(result.current.data).toEqual(mockResponse);
});
it('should handle payment intent creation errors', async () => {
const mockError = new Error('Failed to create payment intent');
vi.mocked(apiClient.post).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useCreatePaymentIntent(), { wrapper });
result.current.mutate(50000);
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useConfirmPayment', () => {
it('should confirm payment successfully', async () => {
const mockResponse = {
success: true,
balance_cents: 100000,
transaction_id: 123,
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useConfirmPayment(), { wrapper });
result.current.mutate({
payment_intent_id: 'pi_test123',
save_payment_method: true,
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/confirm-payment/', {
payment_intent_id: 'pi_test123',
save_payment_method: true,
});
expect(result.current.data).toEqual(mockResponse);
});
it('should invalidate credits and transactions queries on success', async () => {
const mockResponse = { success: true };
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const { result } = renderHook(() => useConfirmPayment(), { wrapper });
result.current.mutate({ payment_intent_id: 'pi_test123' });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['communicationCredits'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['creditTransactions'] });
});
it('should handle confirmation errors', async () => {
const mockError = new Error('Payment confirmation failed');
vi.mocked(apiClient.post).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useConfirmPayment(), { wrapper });
result.current.mutate({ payment_intent_id: 'pi_test123' });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useSetupPaymentMethod', () => {
it('should setup payment method successfully', async () => {
const mockResponse = {
client_secret: 'seti_test_secret',
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useSetupPaymentMethod(), { wrapper });
result.current.mutate();
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/setup-payment-method/');
expect(result.current.data).toEqual(mockResponse);
});
it('should handle setup errors', async () => {
const mockError = new Error('Failed to setup payment method');
vi.mocked(apiClient.post).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useSetupPaymentMethod(), { wrapper });
result.current.mutate();
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useSavePaymentMethod', () => {
it('should save payment method successfully', async () => {
const mockResponse = {
success: true,
payment_method_id: 'pm_test123',
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useSavePaymentMethod(), { wrapper });
result.current.mutate('pm_test123');
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/save-payment-method/', {
payment_method_id: 'pm_test123',
});
expect(result.current.data).toEqual(mockResponse);
});
it('should invalidate credits query on success', async () => {
const mockResponse = { success: true };
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const { result } = renderHook(() => useSavePaymentMethod(), { wrapper });
result.current.mutate('pm_test123');
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['communicationCredits'] });
});
it('should handle save errors', async () => {
const mockError = new Error('Failed to save payment method');
vi.mocked(apiClient.post).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useSavePaymentMethod(), { wrapper });
result.current.mutate('pm_test123');
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useCommunicationUsageStats', () => {
it('should fetch usage stats successfully', async () => {
const mockStats = {
sms_sent_this_month: 150,
voice_minutes_this_month: 45.5,
proxy_numbers_active: 2,
estimated_cost_cents: 2500,
};
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockStats });
const { result } = renderHook(() => useCommunicationUsageStats(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.get).toHaveBeenCalledWith('/communication-credits/usage-stats/');
expect(result.current.data).toEqual(mockStats);
});
it('should handle fetch errors', async () => {
const mockError = new Error('Failed to fetch stats');
vi.mocked(apiClient.get).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useCommunicationUsageStats(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
it('should use correct query key', async () => {
const mockStats = {
sms_sent_this_month: 150,
voice_minutes_this_month: 45.5,
proxy_numbers_active: 2,
estimated_cost_cents: 2500,
};
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockStats });
const { result } = renderHook(() => useCommunicationUsageStats(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
const cachedData = queryClient.getQueryData(['communicationUsageStats']);
expect(cachedData).toEqual(mockStats);
});
});
describe('usePhoneNumbers', () => {
it('should fetch phone numbers successfully', async () => {
const mockResponse = {
numbers: [mockPhoneNumber],
count: 1,
};
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => usePhoneNumbers(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.get).toHaveBeenCalledWith('/communication-credits/phone-numbers/');
expect(result.current.data).toEqual(mockResponse);
});
it('should handle fetch errors', async () => {
const mockError = new Error('Failed to fetch phone numbers');
vi.mocked(apiClient.get).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => usePhoneNumbers(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useSearchPhoneNumbers', () => {
it('should search phone numbers successfully', async () => {
const mockResponse = {
numbers: [mockAvailableNumber],
count: 1,
};
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useSearchPhoneNumbers(), { wrapper });
result.current.mutate({
area_code: '555',
country: 'US',
limit: 10,
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.get).toHaveBeenCalledWith('/communication-credits/phone-numbers/search/', {
params: {
area_code: '555',
country: 'US',
limit: 10,
},
});
expect(result.current.data).toEqual(mockResponse);
});
it('should support contains parameter', async () => {
const mockResponse = {
numbers: [mockAvailableNumber],
count: 1,
};
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useSearchPhoneNumbers(), { wrapper });
result.current.mutate({
contains: '123',
country: 'US',
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.get).toHaveBeenCalledWith('/communication-credits/phone-numbers/search/', {
params: {
contains: '123',
country: 'US',
},
});
});
it('should handle search errors', async () => {
const mockError = new Error('Search failed');
vi.mocked(apiClient.get).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useSearchPhoneNumbers(), { wrapper });
result.current.mutate({ area_code: '555' });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('usePurchasePhoneNumber', () => {
it('should purchase phone number successfully', async () => {
const mockResponse = {
success: true,
phone_number: mockPhoneNumber,
balance_cents: 49900,
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => usePurchasePhoneNumber(), { wrapper });
result.current.mutate({
phone_number: '+15551234567',
friendly_name: 'Main Office Line',
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/phone-numbers/purchase/', {
phone_number: '+15551234567',
friendly_name: 'Main Office Line',
});
expect(result.current.data).toEqual(mockResponse);
});
it('should invalidate queries on success', async () => {
const mockResponse = {
success: true,
phone_number: mockPhoneNumber,
balance_cents: 49900,
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const { result } = renderHook(() => usePurchasePhoneNumber(), { wrapper });
result.current.mutate({ phone_number: '+15551234567' });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['phoneNumbers'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['communicationCredits'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['creditTransactions'] });
});
it('should handle purchase errors', async () => {
const mockError = new Error('Insufficient credits');
vi.mocked(apiClient.post).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => usePurchasePhoneNumber(), { wrapper });
result.current.mutate({ phone_number: '+15551234567' });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useReleasePhoneNumber', () => {
it('should release phone number successfully', async () => {
const mockResponse = {
success: true,
message: 'Phone number released successfully',
};
vi.mocked(apiClient.delete).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useReleasePhoneNumber(), { wrapper });
result.current.mutate(1);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.delete).toHaveBeenCalledWith('/communication-credits/phone-numbers/1/');
expect(result.current.data).toEqual(mockResponse);
});
it('should invalidate queries on success', async () => {
const mockResponse = {
success: true,
message: 'Phone number released successfully',
};
vi.mocked(apiClient.delete).mockResolvedValueOnce({ data: mockResponse });
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const { result } = renderHook(() => useReleasePhoneNumber(), { wrapper });
result.current.mutate(1);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['phoneNumbers'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['communicationUsageStats'] });
});
it('should handle release errors', async () => {
const mockError = new Error('Failed to release phone number');
vi.mocked(apiClient.delete).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useReleasePhoneNumber(), { wrapper });
result.current.mutate(1);
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
});
describe('useChangePhoneNumber', () => {
it('should change phone number successfully', async () => {
const newPhoneNumber = {
...mockPhoneNumber,
phone_number: '+15559876543',
friendly_name: 'Updated Office Line',
};
const mockResponse = {
success: true,
phone_number: newPhoneNumber,
balance_cents: 49900,
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useChangePhoneNumber(), { wrapper });
result.current.mutate({
numberId: 1,
new_phone_number: '+15559876543',
friendly_name: 'Updated Office Line',
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/phone-numbers/1/change/', {
new_phone_number: '+15559876543',
friendly_name: 'Updated Office Line',
});
expect(result.current.data).toEqual(mockResponse);
});
it('should invalidate queries on success', async () => {
const mockResponse = {
success: true,
phone_number: mockPhoneNumber,
balance_cents: 49900,
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const { result } = renderHook(() => useChangePhoneNumber(), { wrapper });
result.current.mutate({
numberId: 1,
new_phone_number: '+15559876543',
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['phoneNumbers'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['communicationCredits'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['creditTransactions'] });
});
it('should handle change errors', async () => {
const mockError = new Error('Failed to change phone number');
vi.mocked(apiClient.post).mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useChangePhoneNumber(), { wrapper });
result.current.mutate({
numberId: 1,
new_phone_number: '+15559876543',
});
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toEqual(mockError);
});
it('should exclude numberId from request body', async () => {
const mockResponse = {
success: true,
phone_number: mockPhoneNumber,
balance_cents: 49900,
};
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useChangePhoneNumber(), { wrapper });
result.current.mutate({
numberId: 1,
new_phone_number: '+15559876543',
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
// Verify numberId is NOT in the request body
expect(apiClient.post).toHaveBeenCalledWith('/communication-credits/phone-numbers/1/change/', {
new_phone_number: '+15559876543',
});
});
});
describe('Integration Tests', () => {
it('should update credits after adding credits', async () => {
const initialCredits = mockCredits;
const updatedCredits = { ...mockCredits, balance_cents: 100000 };
// Initial fetch
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: initialCredits });
const { result: creditsResult } = renderHook(() => useCommunicationCredits(), { wrapper });
await waitFor(() => expect(creditsResult.current.isSuccess).toBe(true));
expect(creditsResult.current.data?.balance_cents).toBe(50000);
// Add credits
vi.mocked(apiClient.post).mockResolvedValueOnce({ data: { success: true } });
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: updatedCredits });
const { result: addResult } = renderHook(() => useAddCredits(), { wrapper });
addResult.current.mutate({ amount_cents: 50000 });
await waitFor(() => expect(addResult.current.isSuccess).toBe(true));
// Refetch credits
await creditsResult.current.refetch();
expect(creditsResult.current.data?.balance_cents).toBe(100000);
});
it('should update phone numbers list after purchasing', async () => {
const initialResponse = { numbers: [], count: 0 };
const updatedResponse = { numbers: [mockPhoneNumber], count: 1 };
// Initial fetch
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: initialResponse });
const { result: numbersResult } = renderHook(() => usePhoneNumbers(), { wrapper });
await waitFor(() => expect(numbersResult.current.isSuccess).toBe(true));
expect(numbersResult.current.data?.count).toBe(0);
// Purchase number
vi.mocked(apiClient.post).mockResolvedValueOnce({
data: {
success: true,
phone_number: mockPhoneNumber,
balance_cents: 49900,
},
});
vi.mocked(apiClient.get).mockResolvedValueOnce({ data: updatedResponse });
const { result: purchaseResult } = renderHook(() => usePurchasePhoneNumber(), { wrapper });
purchaseResult.current.mutate({ phone_number: '+15551234567' });
await waitFor(() => expect(purchaseResult.current.isSuccess).toBe(true));
// Refetch numbers
await numbersResult.current.refetch();
expect(numbersResult.current.data?.count).toBe(1);
expect(numbersResult.current.data?.numbers[0]).toEqual(mockPhoneNumber);
});
});
});