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(); }); }); });