/** * Tests for TwoFactorVerify Component * FE-TEST-005: Test two-factor verification component */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TwoFactorVerify } from './TwoFactorVerify'; import { twoFactorService } from '@/services/2fa-service'; import { useToast } from '@/hooks/useToast'; // Mock dependencies vi.mock('@/services/2fa-service', () => ({ twoFactorService: { verify: vi.fn(), }, })); vi.mock('@/hooks/useToast', () => ({ useToast: vi.fn(), })); describe('TwoFactorVerify', () => { const mockOnSuccess = vi.fn(); const mockOnCancel = vi.fn(); const mockToast = vi.fn(); beforeEach(() => { vi.clearAllMocks(); vi.mocked(useToast).mockReturnValue({ toast: mockToast, } as any); }); it('should render two-factor verification form', () => { render( , ); expect(screen.getByText('Two-Factor Authentication')).toBeInTheDocument(); expect( screen.getByText('Enter the code from your authenticator app'), ).toBeInTheDocument(); expect(screen.getByLabelText('Verification Code')).toBeInTheDocument(); }); it('should allow entering verification code', async () => { const user = userEvent.setup(); render( , ); const codeInput = screen.getByLabelText('Verification Code'); await user.type(codeInput, '123456'); expect(codeInput).toHaveValue('123456'); }); it('should only allow numeric input', async () => { const user = userEvent.setup(); render( , ); const codeInput = screen.getByLabelText('Verification Code'); await user.type(codeInput, 'abc123def456'); expect(codeInput).toHaveValue('123456'); }); it('should limit code to 6 digits', async () => { const user = userEvent.setup(); render( , ); const codeInput = screen.getByLabelText('Verification Code'); await user.type(codeInput, '1234567890'); expect(codeInput).toHaveValue('123456'); }); it('should verify code on submit', async () => { const user = userEvent.setup(); vi.mocked(twoFactorService.verify).mockResolvedValue(undefined); render( , ); const codeInput = screen.getByLabelText('Verification Code'); const verifyButton = screen.getByRole('button', { name: /verify/i }); await user.type(codeInput, '123456'); await user.click(verifyButton); await waitFor(() => { expect(twoFactorService.verify).toHaveBeenCalledWith('', '123456'); expect(mockOnSuccess).toHaveBeenCalledWith('123456'); }); }); it('should show error for invalid code', async () => { const user = userEvent.setup(); vi.mocked(twoFactorService.verify).mockRejectedValue( new Error('Invalid verification code'), ); render( , ); const codeInput = screen.getByLabelText('Verification Code'); const verifyButton = screen.getByRole('button', { name: /verify/i }); await user.type(codeInput, '123456'); await user.click(verifyButton); await waitFor(() => { expect( screen.getByText(/invalid verification code/i), ).toBeInTheDocument(); }); }); it('should show error on verification failure', async () => { const user = userEvent.setup(); vi.mocked(twoFactorService.verify).mockRejectedValue( new Error('Verification failed'), ); render( , ); const codeInput = screen.getByLabelText('Verification Code'); const verifyButton = screen.getByRole('button', { name: /verify/i }); await user.type(codeInput, '123456'); await user.click(verifyButton); await waitFor(() => { expect(screen.getByText('Verification failed')).toBeInTheDocument(); expect(mockToast).toHaveBeenCalled(); }); }); it('should show loading state during verification', async () => { const user = userEvent.setup(); vi.mocked(twoFactorService.verify).mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve(undefined), 100)), ); render( , ); const codeInput = screen.getByLabelText('Verification Code'); const verifyButton = screen.getByRole('button', { name: /verify/i }); await user.type(codeInput, '123456'); await user.click(verifyButton); expect(screen.getByText('Verifying...')).toBeInTheDocument(); expect(verifyButton).toBeDisabled(); }); it('should switch to backup code mode', async () => { const user = userEvent.setup(); render( , ); const useBackupLink = screen.getByText(/use a backup code/i); await user.click(useBackupLink); expect(screen.getByLabelText('Backup Code')).toBeInTheDocument(); expect( screen.queryByLabelText('Verification Code'), ).not.toBeInTheDocument(); }); it('should switch back to regular code mode', async () => { const user = userEvent.setup(); render( , ); // Switch to backup code const useBackupLink = screen.getByText(/use a backup code/i); await user.click(useBackupLink); // Switch back const useAuthLink = screen.getByText(/use authenticator code instead/i); await user.click(useAuthLink); expect(screen.getByLabelText('Verification Code')).toBeInTheDocument(); expect(screen.queryByLabelText('Backup Code')).not.toBeInTheDocument(); }); it('should verify backup code', async () => { const user = userEvent.setup(); vi.mocked(twoFactorService.verify).mockResolvedValue(undefined); render( , ); // Switch to backup code const useBackupLink = screen.getByText(/use a backup code/i); await user.click(useBackupLink); const backupInput = screen.getByLabelText('Backup Code'); const verifyButton = screen.getByRole('button', { name: /verify/i }); await user.type(backupInput, 'backup-code-123'); await user.click(verifyButton); await waitFor(() => { expect(twoFactorService.verify).toHaveBeenCalledWith( '', 'backup-code-123', ); expect(mockOnSuccess).toHaveBeenCalledWith('backup-code-123'); }); }); it('should call onCancel when cancel button is clicked', async () => { const user = userEvent.setup(); render( , ); const cancelButton = screen.getByRole('button', { name: /cancel/i }); await user.click(cancelButton); expect(mockOnCancel).toHaveBeenCalled(); }); it('should disable verify button when no code entered', () => { render( , ); const verifyButton = screen.getByRole('button', { name: /verify/i }); expect(verifyButton).toBeDisabled(); }); it('should show error when trying to verify without code', async () => { const user = userEvent.setup(); render( , ); const verifyButton = screen.getByRole('button', { name: /verify/i }); // Button should be disabled, but let's test the error handling // by manually triggering the handler const codeInput = screen.getByLabelText('Verification Code'); fireEvent.change(codeInput, { target: { value: '' } }); // The button should remain disabled expect(verifyButton).toBeDisabled(); }); });