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

462 lines
13 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 profile API
vi.mock('../../api/profile', () => ({
getProfile: vi.fn(),
updateProfile: vi.fn(),
uploadAvatar: vi.fn(),
deleteAvatar: vi.fn(),
sendVerificationEmail: vi.fn(),
verifyEmail: vi.fn(),
requestEmailChange: vi.fn(),
confirmEmailChange: vi.fn(),
changePassword: vi.fn(),
setupTOTP: vi.fn(),
verifyTOTP: vi.fn(),
disableTOTP: vi.fn(),
getRecoveryCodes: vi.fn(),
regenerateRecoveryCodes: vi.fn(),
sendPhoneVerification: vi.fn(),
verifyPhoneCode: vi.fn(),
getSessions: vi.fn(),
revokeSession: vi.fn(),
revokeOtherSessions: vi.fn(),
getLoginHistory: vi.fn(),
getUserEmails: vi.fn(),
addUserEmail: vi.fn(),
deleteUserEmail: vi.fn(),
sendUserEmailVerification: vi.fn(),
verifyUserEmail: vi.fn(),
setPrimaryEmail: vi.fn(),
}));
import {
useProfile,
useUpdateProfile,
useUploadAvatar,
useDeleteAvatar,
useSendVerificationEmail,
useVerifyEmail,
useRequestEmailChange,
useConfirmEmailChange,
useChangePassword,
useSetupTOTP,
useVerifyTOTP,
useDisableTOTP,
useRegenerateRecoveryCodes,
useSendPhoneVerification,
useVerifyPhoneCode,
useSessions,
useRevokeSession,
useRevokeOtherSessions,
useLoginHistory,
useUserEmails,
useAddUserEmail,
useDeleteUserEmail,
useSendUserEmailVerification,
useVerifyUserEmail,
useSetPrimaryEmail,
} from '../useProfile';
import * as profileApi from '../../api/profile';
// 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('useProfile hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useProfile', () => {
it('fetches user profile', async () => {
const mockProfile = { id: 1, name: 'Test User', email: 'test@example.com' };
vi.mocked(profileApi.getProfile).mockResolvedValue(mockProfile as any);
const { result } = renderHook(() => useProfile(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockProfile);
});
});
describe('useUpdateProfile', () => {
it('updates profile', async () => {
const mockUpdated = { id: 1, name: 'Updated' };
vi.mocked(profileApi.updateProfile).mockResolvedValue(mockUpdated as any);
const { result } = renderHook(() => useUpdateProfile(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({ name: 'Updated' });
});
expect(profileApi.updateProfile).toHaveBeenCalled();
});
});
describe('useUploadAvatar', () => {
it('uploads avatar', async () => {
vi.mocked(profileApi.uploadAvatar).mockResolvedValue({ avatar_url: 'url' });
const { result } = renderHook(() => useUploadAvatar(), {
wrapper: createWrapper(),
});
const file = new File(['test'], 'avatar.jpg');
await act(async () => {
await result.current.mutateAsync(file);
});
expect(profileApi.uploadAvatar).toHaveBeenCalled();
});
});
describe('useDeleteAvatar', () => {
it('deletes avatar', async () => {
vi.mocked(profileApi.deleteAvatar).mockResolvedValue(undefined);
const { result } = renderHook(() => useDeleteAvatar(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync();
});
expect(profileApi.deleteAvatar).toHaveBeenCalled();
});
});
describe('email hooks', () => {
it('sends verification email', async () => {
vi.mocked(profileApi.sendVerificationEmail).mockResolvedValue(undefined);
const { result } = renderHook(() => useSendVerificationEmail(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync();
});
expect(profileApi.sendVerificationEmail).toHaveBeenCalled();
});
it('verifies email', async () => {
vi.mocked(profileApi.verifyEmail).mockResolvedValue(undefined);
const { result } = renderHook(() => useVerifyEmail(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('token');
});
expect(profileApi.verifyEmail).toHaveBeenCalled();
});
it('requests email change', async () => {
vi.mocked(profileApi.requestEmailChange).mockResolvedValue(undefined);
const { result } = renderHook(() => useRequestEmailChange(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('new@example.com');
});
expect(profileApi.requestEmailChange).toHaveBeenCalled();
});
it('confirms email change', async () => {
vi.mocked(profileApi.confirmEmailChange).mockResolvedValue(undefined);
const { result } = renderHook(() => useConfirmEmailChange(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('token');
});
expect(profileApi.confirmEmailChange).toHaveBeenCalled();
});
});
describe('useChangePassword', () => {
it('changes password', async () => {
vi.mocked(profileApi.changePassword).mockResolvedValue(undefined);
const { result } = renderHook(() => useChangePassword(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
currentPassword: 'old',
newPassword: 'new',
});
});
expect(profileApi.changePassword).toHaveBeenCalled();
});
});
describe('2FA hooks', () => {
it('sets up TOTP', async () => {
const mockSetup = { secret: 'ABC', qr_code: 'qr', provisioning_uri: 'uri' };
vi.mocked(profileApi.setupTOTP).mockResolvedValue(mockSetup);
const { result } = renderHook(() => useSetupTOTP(), {
wrapper: createWrapper(),
});
await act(async () => {
const data = await result.current.mutateAsync();
expect(data).toEqual(mockSetup);
});
});
it('verifies TOTP', async () => {
vi.mocked(profileApi.verifyTOTP).mockResolvedValue({ success: true, recovery_codes: [] });
const { result } = renderHook(() => useVerifyTOTP(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('123456');
});
expect(profileApi.verifyTOTP).toHaveBeenCalled();
});
it('disables TOTP', async () => {
vi.mocked(profileApi.disableTOTP).mockResolvedValue(undefined);
const { result } = renderHook(() => useDisableTOTP(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('123456');
});
expect(profileApi.disableTOTP).toHaveBeenCalled();
});
it('regenerates recovery codes', async () => {
vi.mocked(profileApi.regenerateRecoveryCodes).mockResolvedValue(['code1', 'code2']);
const { result } = renderHook(() => useRegenerateRecoveryCodes(), {
wrapper: createWrapper(),
});
await act(async () => {
const codes = await result.current.mutateAsync();
expect(codes).toEqual(['code1', 'code2']);
});
});
});
describe('phone hooks', () => {
it('sends phone verification', async () => {
vi.mocked(profileApi.sendPhoneVerification).mockResolvedValue(undefined);
const { result } = renderHook(() => useSendPhoneVerification(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('555-1234');
});
expect(profileApi.sendPhoneVerification).toHaveBeenCalled();
});
it('verifies phone code', async () => {
vi.mocked(profileApi.verifyPhoneCode).mockResolvedValue(undefined);
const { result } = renderHook(() => useVerifyPhoneCode(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('123456');
});
expect(profileApi.verifyPhoneCode).toHaveBeenCalled();
});
});
describe('session hooks', () => {
it('fetches sessions', async () => {
const mockSessions = [{ id: '1', device_info: 'Chrome' }];
vi.mocked(profileApi.getSessions).mockResolvedValue(mockSessions as any);
const { result } = renderHook(() => useSessions(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockSessions);
});
it('revokes session', async () => {
vi.mocked(profileApi.revokeSession).mockResolvedValue(undefined);
const { result } = renderHook(() => useRevokeSession(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('session-id');
});
expect(profileApi.revokeSession).toHaveBeenCalled();
});
it('revokes other sessions', async () => {
vi.mocked(profileApi.revokeOtherSessions).mockResolvedValue(undefined);
const { result } = renderHook(() => useRevokeOtherSessions(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync();
});
expect(profileApi.revokeOtherSessions).toHaveBeenCalled();
});
it('fetches login history', async () => {
const mockHistory = [{ id: '1', success: true }];
vi.mocked(profileApi.getLoginHistory).mockResolvedValue(mockHistory as any);
const { result } = renderHook(() => useLoginHistory(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockHistory);
});
});
describe('multiple email hooks', () => {
it('fetches user emails', async () => {
const mockEmails = [{ id: 1, email: 'test@example.com' }];
vi.mocked(profileApi.getUserEmails).mockResolvedValue(mockEmails as any);
const { result } = renderHook(() => useUserEmails(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockEmails);
});
it('adds user email', async () => {
vi.mocked(profileApi.addUserEmail).mockResolvedValue({ id: 2, email: 'new@example.com' } as any);
const { result } = renderHook(() => useAddUserEmail(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('new@example.com');
});
expect(profileApi.addUserEmail).toHaveBeenCalled();
});
it('deletes user email', async () => {
vi.mocked(profileApi.deleteUserEmail).mockResolvedValue(undefined);
const { result } = renderHook(() => useDeleteUserEmail(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(2);
});
expect(profileApi.deleteUserEmail).toHaveBeenCalled();
});
it('sends user email verification', async () => {
vi.mocked(profileApi.sendUserEmailVerification).mockResolvedValue(undefined);
const { result } = renderHook(() => useSendUserEmailVerification(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(2);
});
expect(profileApi.sendUserEmailVerification).toHaveBeenCalled();
});
it('verifies user email', async () => {
vi.mocked(profileApi.verifyUserEmail).mockResolvedValue(undefined);
const { result } = renderHook(() => useVerifyUserEmail(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({ emailId: 2, token: 'token' });
});
expect(profileApi.verifyUserEmail).toHaveBeenCalled();
});
it('sets primary email', async () => {
vi.mocked(profileApi.setPrimaryEmail).mockResolvedValue(undefined);
const { result } = renderHook(() => useSetPrimaryEmail(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(2);
});
expect(profileApi.setPrimaryEmail).toHaveBeenCalled();
});
});
});