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

350 lines
10 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 dependencies
vi.mock('../../api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
},
}));
vi.mock('../../utils/cookies', () => ({
getCookie: vi.fn(),
}));
import { useCurrentBusiness, useUpdateBusiness, useBusinessUsers, useResources, useCreateResource } from '../useBusiness';
import apiClient from '../../api/client';
import { getCookie } from '../../utils/cookies';
// 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('useBusiness hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useCurrentBusiness', () => {
it('returns null when no token exists', async () => {
vi.mocked(getCookie).mockReturnValue(null);
const { result } = renderHook(() => useCurrentBusiness(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBeNull();
expect(apiClient.get).not.toHaveBeenCalled();
});
it('fetches business and transforms data', async () => {
vi.mocked(getCookie).mockReturnValue('valid-token');
const mockBusiness = {
id: 1,
name: 'Test Business',
subdomain: 'test',
primary_color: '#FF0000',
secondary_color: '#00FF00',
logo_url: 'https://example.com/logo.png',
timezone: 'America/Denver',
timezone_display_mode: 'business',
tier: 'professional',
status: 'active',
created_at: '2024-01-01T00:00:00Z',
payments_enabled: true,
plan_permissions: {
sms_reminders: true,
api_access: true,
},
};
vi.mocked(apiClient.get).mockResolvedValue({ data: mockBusiness });
const { result } = renderHook(() => useCurrentBusiness(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/business/current/');
expect(result.current.data).toEqual(expect.objectContaining({
id: '1',
name: 'Test Business',
subdomain: 'test',
primaryColor: '#FF0000',
secondaryColor: '#00FF00',
logoUrl: 'https://example.com/logo.png',
timezone: 'America/Denver',
plan: 'professional',
paymentsEnabled: true,
}));
});
it('uses default values for missing fields', async () => {
vi.mocked(getCookie).mockReturnValue('valid-token');
const mockBusiness = {
id: 1,
name: 'Minimal Business',
subdomain: 'min',
};
vi.mocked(apiClient.get).mockResolvedValue({ data: mockBusiness });
const { result } = renderHook(() => useCurrentBusiness(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.primaryColor).toBe('#3B82F6');
expect(result.current.data?.secondaryColor).toBe('#1E40AF');
expect(result.current.data?.logoDisplayMode).toBe('text-only');
expect(result.current.data?.timezone).toBe('America/New_York');
expect(result.current.data?.paymentsEnabled).toBe(false);
});
});
describe('useUpdateBusiness', () => {
it('maps frontend fields to backend fields', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
const { result } = renderHook(() => useUpdateBusiness(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
name: 'Updated Name',
primaryColor: '#123456',
secondaryColor: '#654321',
timezone: 'America/Los_Angeles',
});
});
expect(apiClient.patch).toHaveBeenCalledWith('/business/current/update/', {
name: 'Updated Name',
primary_color: '#123456',
secondary_color: '#654321',
timezone: 'America/Los_Angeles',
});
});
it('handles logo fields', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
const { result } = renderHook(() => useUpdateBusiness(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
logoUrl: 'https://new-logo.com/logo.png',
emailLogoUrl: 'https://new-logo.com/email.png',
logoDisplayMode: 'logo-only',
});
});
expect(apiClient.patch).toHaveBeenCalledWith('/business/current/update/', {
logo_url: 'https://new-logo.com/logo.png',
email_logo_url: 'https://new-logo.com/email.png',
logo_display_mode: 'logo-only',
});
});
it('handles booking-related settings', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
const { result } = renderHook(() => useUpdateBusiness(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
resourcesCanReschedule: true,
requirePaymentMethodToBook: true,
cancellationWindowHours: 24,
lateCancellationFeePercent: 50,
});
});
expect(apiClient.patch).toHaveBeenCalledWith('/business/current/update/', {
resources_can_reschedule: true,
require_payment_method_to_book: true,
cancellation_window_hours: 24,
late_cancellation_fee_percent: 50,
});
});
it('handles website and dashboard content', async () => {
vi.mocked(apiClient.patch).mockResolvedValue({ data: {} });
const { result } = renderHook(() => useUpdateBusiness(), {
wrapper: createWrapper(),
});
const websitePages = { home: { title: 'Welcome' } };
const dashboardContent = [{ type: 'text', content: 'Hello' }];
await act(async () => {
await result.current.mutateAsync({
websitePages,
customerDashboardContent: dashboardContent,
initialSetupComplete: true,
});
});
expect(apiClient.patch).toHaveBeenCalledWith('/business/current/update/', {
website_pages: websitePages,
customer_dashboard_content: dashboardContent,
initial_setup_complete: true,
});
});
});
describe('useBusinessUsers', () => {
it('fetches staff users', async () => {
const mockUsers = [
{ id: 1, name: 'Staff 1' },
{ id: 2, name: 'Staff 2' },
];
vi.mocked(apiClient.get).mockResolvedValue({ data: mockUsers });
const { result } = renderHook(() => useBusinessUsers(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/staff/');
expect(result.current.data).toEqual(mockUsers);
});
});
describe('useResources', () => {
it('fetches resources', async () => {
const mockResources = [
{ id: 1, name: 'Resource 1', type: 'equipment' },
{ id: 2, name: 'Resource 2', type: 'room' },
];
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResources });
const { result } = renderHook(() => useResources(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/resources/');
expect(result.current.data).toEqual(mockResources);
});
it('handles empty resources list', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
const { result } = renderHook(() => useResources(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual([]);
});
it('handles fetch error', async () => {
vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() => useResources(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBeDefined();
});
});
describe('useCreateResource', () => {
it('creates a resource', async () => {
const mockResource = { id: 3, name: 'New Resource', type: 'equipment' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResource });
const { result } = renderHook(() => useCreateResource(), {
wrapper: createWrapper(),
});
await act(async () => {
const data = await result.current.mutateAsync({ name: 'New Resource', type: 'equipment' });
expect(data).toEqual(mockResource);
});
expect(apiClient.post).toHaveBeenCalledWith('/resources/', {
name: 'New Resource',
type: 'equipment',
});
});
it('creates a resource with user_id', async () => {
const mockResource = { id: 4, name: 'Staff Resource', type: 'staff', user_id: 'user-123' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResource });
const { result } = renderHook(() => useCreateResource(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({ name: 'Staff Resource', type: 'staff', user_id: 'user-123' });
});
expect(apiClient.post).toHaveBeenCalledWith('/resources/', {
name: 'Staff Resource',
type: 'staff',
user_id: 'user-123',
});
});
it('handles creation error', async () => {
vi.mocked(apiClient.post).mockRejectedValue(new Error('Validation failed'));
const { result } = renderHook(() => useCreateResource(), {
wrapper: createWrapper(),
});
await expect(
act(async () => {
await result.current.mutateAsync({ name: '', type: 'equipment' });
})
).rejects.toThrow('Validation failed');
});
});
});