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

686 lines
19 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(),
},
}));
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);
});
});
});