- 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>
943 lines
30 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|