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(), }, })); import { useUsers, useStaffForAssignment, usePlatformStaffForAssignment, useUpdateStaffPermissions, } from '../useUsers'; 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('useUsers hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useUsers', () => { it('fetches all staff members', async () => { const mockStaff = [ { id: 1, email: 'owner@example.com', name: 'John Owner', username: 'jowner', role: 'owner', is_active: true, permissions: { can_access_resources: true }, can_invite_staff: true, }, { id: 2, email: 'manager@example.com', name: 'Jane Manager', username: 'jmanager', role: 'manager', is_active: true, permissions: { can_access_services: false }, can_invite_staff: false, }, { id: 3, email: 'staff@example.com', name: 'Bob Staff', username: 'bstaff', role: 'staff', is_active: false, permissions: {}, can_invite_staff: false, }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockStaff }); const { result } = renderHook(() => useUsers(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/staff/'); expect(result.current.data).toHaveLength(3); expect(result.current.data).toEqual(mockStaff); }); it('returns empty array when no staff members exist', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const { result } = renderHook(() => useUsers(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/staff/'); expect(result.current.data).toEqual([]); }); it('handles API errors', async () => { const errorMessage = 'Failed to fetch staff'; vi.mocked(apiClient.get).mockRejectedValue(new Error(errorMessage)); const { result } = renderHook(() => useUsers(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isError).toBe(true); }); expect(result.current.error).toBeDefined(); }); it('uses correct query key', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const { result } = renderHook(() => useUsers(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Query key should be ['staff'] for caching and invalidation expect(apiClient.get).toHaveBeenCalledWith('/staff/'); }); }); describe('useStaffForAssignment', () => { it('fetches and transforms staff for dropdown use', async () => { const mockStaff = [ { id: 1, email: 'john@example.com', name: 'John Doe', role: 'owner', is_active: true, permissions: {}, }, { id: 2, email: 'jane@example.com', name: 'Jane Smith', role: 'manager', is_active: true, permissions: {}, }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockStaff }); const { result } = renderHook(() => useStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/staff/'); expect(result.current.data).toEqual([ { id: '1', name: 'John Doe', email: 'john@example.com', role: 'owner', }, { id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'manager', }, ]); }); it('converts id to string', async () => { const mockStaff = [ { id: 123, email: 'test@example.com', name: 'Test User', role: 'staff', is_active: true, permissions: {}, }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockStaff }); const { result } = renderHook(() => useStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0].id).toBe('123'); expect(typeof result.current.data?.[0].id).toBe('string'); }); it('falls back to email when name is not provided', async () => { const mockStaff = [ { id: 1, email: 'noname@example.com', name: null, role: 'staff', is_active: true, permissions: {}, }, { id: 2, email: 'emptyname@example.com', name: '', role: 'staff', is_active: true, permissions: {}, }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockStaff }); const { result } = renderHook(() => useStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0].name).toBe('noname@example.com'); expect(result.current.data?.[1].name).toBe('emptyname@example.com'); }); it('includes all roles (owner, manager, staff)', async () => { const mockStaff = [ { id: 1, email: 'owner@example.com', name: 'Owner User', role: 'owner', is_active: true, permissions: {}, }, { id: 2, email: 'manager@example.com', name: 'Manager User', role: 'manager', is_active: true, permissions: {}, }, { id: 3, email: 'staff@example.com', name: 'Staff User', role: 'staff', is_active: true, permissions: {}, }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockStaff }); const { result } = renderHook(() => useStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toHaveLength(3); expect(result.current.data?.map(u => u.role)).toEqual(['owner', 'manager', 'staff']); }); it('returns empty array when no staff exist', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const { result } = renderHook(() => useStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual([]); }); }); describe('usePlatformStaffForAssignment', () => { it('fetches and filters platform staff by role', async () => { const mockPlatformUsers = [ { id: 1, email: 'super@platform.com', name: 'Super User', role: 'superuser', }, { id: 2, email: 'manager@platform.com', name: 'Platform Manager', role: 'platform_manager', }, { id: 3, email: 'support@platform.com', name: 'Platform Support', role: 'platform_support', }, { id: 4, email: 'owner@business.com', name: 'Business Owner', role: 'owner', }, { id: 5, email: 'staff@business.com', name: 'Business Staff', role: 'staff', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlatformUsers }); const { result } = renderHook(() => usePlatformStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/platform/users/'); // Should only return platform roles expect(result.current.data).toHaveLength(3); expect(result.current.data?.map(u => u.role)).toEqual([ 'superuser', 'platform_manager', 'platform_support', ]); }); it('transforms platform users for dropdown use', async () => { const mockPlatformUsers = [ { id: 10, email: 'admin@platform.com', name: 'Admin User', role: 'superuser', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlatformUsers }); const { result } = renderHook(() => usePlatformStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual([ { id: '10', name: 'Admin User', email: 'admin@platform.com', role: 'superuser', }, ]); }); it('filters out non-platform roles', async () => { const mockPlatformUsers = [ { id: 1, email: 'super@platform.com', name: 'Super', role: 'superuser' }, { id: 2, email: 'owner@business.com', name: 'Owner', role: 'owner' }, { id: 3, email: 'manager@business.com', name: 'Manager', role: 'manager' }, { id: 4, email: 'staff@business.com', name: 'Staff', role: 'staff' }, { id: 5, email: 'resource@business.com', name: 'Resource', role: 'resource' }, { id: 6, email: 'customer@business.com', name: 'Customer', role: 'customer' }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlatformUsers }); const { result } = renderHook(() => usePlatformStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); // Only superuser should be included from the mock data expect(result.current.data).toHaveLength(1); expect(result.current.data?.[0].role).toBe('superuser'); }); it('includes all three platform roles', async () => { const mockPlatformUsers = [ { id: 1, email: 'super@platform.com', name: 'Super', role: 'superuser' }, { id: 2, email: 'pm@platform.com', name: 'PM', role: 'platform_manager' }, { id: 3, email: 'support@platform.com', name: 'Support', role: 'platform_support' }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlatformUsers }); const { result } = renderHook(() => usePlatformStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); const roles = result.current.data?.map(u => u.role); expect(roles).toContain('superuser'); expect(roles).toContain('platform_manager'); expect(roles).toContain('platform_support'); }); it('falls back to email when name is missing', async () => { const mockPlatformUsers = [ { id: 1, email: 'noname@platform.com', role: 'superuser', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlatformUsers }); const { result } = renderHook(() => usePlatformStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0].name).toBe('noname@platform.com'); }); it('returns empty array when no platform users exist', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); const { result } = renderHook(() => usePlatformStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual([]); }); it('returns empty array when only non-platform users exist', async () => { const mockPlatformUsers = [ { id: 1, email: 'owner@business.com', name: 'Owner', role: 'owner' }, { id: 2, email: 'staff@business.com', name: 'Staff', role: 'staff' }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlatformUsers }); const { result } = renderHook(() => usePlatformStaffForAssignment(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data).toEqual([]); }); }); describe('useUpdateStaffPermissions', () => { it('updates staff permissions', async () => { const updatedStaff = { id: 5, email: 'staff@example.com', name: 'Staff User', role: 'staff', is_active: true, permissions: { can_access_resources: true, can_access_services: false, }, }; vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedStaff }); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ userId: 5, permissions: { can_access_resources: true, can_access_services: false, }, }); }); expect(apiClient.patch).toHaveBeenCalledWith('/staff/5/', { permissions: { can_access_resources: true, can_access_services: false, }, }); }); it('accepts string userId', async () => { vi.mocked(apiClient.patch).mockResolvedValue({ data: {} }); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ userId: '42', permissions: { can_access_resources: true }, }); }); expect(apiClient.patch).toHaveBeenCalledWith('/staff/42/', { permissions: { can_access_resources: true }, }); }); it('accepts number userId', async () => { vi.mocked(apiClient.patch).mockResolvedValue({ data: {} }); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ userId: 123, permissions: { can_list_customers: true }, }); }); expect(apiClient.patch).toHaveBeenCalledWith('/staff/123/', { permissions: { can_list_customers: true }, }); }); it('can update multiple permissions at once', async () => { vi.mocked(apiClient.patch).mockResolvedValue({ data: {} }); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper: createWrapper(), }); const permissions = { can_access_resources: true, can_access_services: true, can_list_customers: false, can_access_scheduled_tasks: false, }; await act(async () => { await result.current.mutateAsync({ userId: 1, permissions, }); }); expect(apiClient.patch).toHaveBeenCalledWith('/staff/1/', { permissions, }); }); it('can set permissions to empty object', async () => { vi.mocked(apiClient.patch).mockResolvedValue({ data: {} }); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ userId: 1, permissions: {}, }); }); expect(apiClient.patch).toHaveBeenCalledWith('/staff/1/', { permissions: {}, }); }); it('invalidates staff query on success', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); vi.mocked(apiClient.patch).mockResolvedValue({ data: {} }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper, }); await act(async () => { await result.current.mutateAsync({ userId: 1, permissions: { can_access_resources: true }, }); }); await waitFor(() => { expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['staff'] }); }); }); it('handles API errors', async () => { const errorMessage = 'Permission update failed'; vi.mocked(apiClient.patch).mockRejectedValue(new Error(errorMessage)); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper: createWrapper(), }); await act(async () => { try { await result.current.mutateAsync({ userId: 1, permissions: { can_access_resources: true }, }); } catch (error) { expect(error).toBeDefined(); } }); await waitFor(() => { expect(result.current.isError).toBe(true); }); }); it('returns updated data from mutation', async () => { const updatedStaff = { id: 10, email: 'updated@example.com', name: 'Updated User', role: 'staff', is_active: true, permissions: { can_access_resources: true, }, }; vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedStaff }); const { result } = renderHook(() => useUpdateStaffPermissions(), { wrapper: createWrapper(), }); let mutationResult; await act(async () => { mutationResult = await result.current.mutateAsync({ userId: 10, permissions: { can_access_resources: true }, }); }); expect(mutationResult).toEqual(updatedStaff); }); }); });