// src/pages/admin/components/AuthView.test.tsx import React from 'react'; import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { AuthView } from './AuthView'; import * as apiClient from '../../../services/apiClient'; import { notifySuccess, notifyError } from '../../../services/notificationService'; import { createMockUserProfile } from '../../../tests/utils/mockFactories'; const mockedApiClient = vi.mocked(apiClient, true); const mockOnClose = vi.fn(); const mockOnLoginSuccess = vi.fn(); vi.mock('../../../components/PasswordInput', () => ({ // Mock the moved component PasswordInput: (props: any) => , })); const defaultProps = { onClose: mockOnClose, onLoginSuccess: mockOnLoginSuccess, }; const setupSuccessMocks = () => { const mockAuthResponse = { userprofile: createMockUserProfile({ user: { user_id: '123', email: 'test@example.com' } }), token: 'mock-token', }; (mockedApiClient.loginUser as Mock).mockResolvedValue( new Response(JSON.stringify(mockAuthResponse)), ); (mockedApiClient.registerUser as Mock).mockResolvedValue( new Response(JSON.stringify(mockAuthResponse)), ); (mockedApiClient.requestPasswordReset as Mock).mockResolvedValue( new Response(JSON.stringify({ message: 'Password reset email sent.' })), ); }; describe('AuthView', () => { beforeEach(() => { vi.clearAllMocks(); setupSuccessMocks(); }); describe('Initial Render and Login', () => { it('should render the Sign In form by default', () => { render(); expect(screen.getByRole('heading', { name: /sign in/i })).toBeInTheDocument(); expect(screen.getByLabelText(/email address/i)).toBeInTheDocument(); expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /sign in$/i })).toBeInTheDocument(); }); it('should allow typing in email and password fields', () => { render(); const emailInput = screen.getByLabelText(/email address/i); const passwordInput = screen.getByLabelText(/^password$/i); fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); fireEvent.change(passwordInput, { target: { value: 'password123' } }); expect(emailInput).toHaveValue('test@example.com'); expect(passwordInput).toHaveValue('password123'); }); it('should call loginUser and onLoginSuccess on successful login', async () => { render(); fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'test@example.com' }, }); fireEvent.change(screen.getByLabelText(/^password$/i), { target: { value: 'password123' } }); fireEvent.click(screen.getByRole('checkbox', { name: /remember me/i })); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { expect(mockedApiClient.loginUser).toHaveBeenCalledWith( 'test@example.com', 'password123', true, expect.any(AbortSignal), ); expect(mockOnLoginSuccess).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ user_id: '123', email: 'test@example.com' }), }), 'mock-token', true, ); expect(mockOnClose).toHaveBeenCalled(); }); }); it('should display an error on failed login', async () => { (mockedApiClient.loginUser as Mock).mockRejectedValueOnce(new Error('Invalid credentials')); render(); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { expect(notifyError).toHaveBeenCalledWith('Invalid credentials'); }); expect(mockOnLoginSuccess).not.toHaveBeenCalled(); }); it('should display an error on non-OK login response', async () => { (mockedApiClient.loginUser as Mock).mockResolvedValueOnce( new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 }), ); render(); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { // useApi hook should parse the error message from the JSON body expect(notifyError).toHaveBeenCalledWith('Unauthorized'); }); expect(mockOnLoginSuccess).not.toHaveBeenCalled(); }); }); describe('Registration', () => { it('should switch to the registration form', () => { render(); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); expect(screen.getByRole('heading', { name: /create an account/i })).toBeInTheDocument(); expect(screen.getByLabelText(/full name/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /^register$/i })).toBeInTheDocument(); }); it('should call registerUser on successful registration', async () => { render(); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'Test User' } }); fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'new@example.com' }, }); fireEvent.change(screen.getByLabelText(/^password$/i), { target: { value: 'newpassword' } }); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { expect(mockedApiClient.registerUser).toHaveBeenCalledWith( 'new@example.com', 'newpassword', 'Test User', '', expect.any(AbortSignal), ); expect(mockOnLoginSuccess).toHaveBeenCalledWith( expect.objectContaining({ user: expect.objectContaining({ user_id: '123' }) }), 'mock-token', false, // rememberMe is false for registration ); expect(mockOnClose).toHaveBeenCalled(); }); }); it('should allow registration without providing a full name', async () => { render(); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); // Do not fill in the full name, which is marked as optional fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'noname@example.com' }, }); fireEvent.change(screen.getByLabelText(/^password$/i), { target: { value: 'password' } }); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { // Verify that registerUser was called with an empty string for the full name expect(mockedApiClient.registerUser).toHaveBeenCalledWith( 'noname@example.com', 'password', '', '', expect.any(AbortSignal), ); expect(mockOnLoginSuccess).toHaveBeenCalled(); }); }); it('should display an error on failed registration', async () => { (mockedApiClient.registerUser as Mock).mockRejectedValueOnce( new Error('Email already exists'), ); render(); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { expect(notifyError).toHaveBeenCalledWith('Email already exists'); }); }); it('should display an error on non-OK registration response', async () => { (mockedApiClient.registerUser as Mock).mockResolvedValueOnce( new Response(JSON.stringify({ message: 'User exists' }), { status: 409 }), ); render(); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { expect(notifyError).toHaveBeenCalledWith('User exists'); }); }); }); describe('Forgot Password', () => { it('should switch to the reset password form', () => { render(); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); expect(screen.getByRole('heading', { name: /reset password/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /send reset link/i })).toBeInTheDocument(); }); it('should call requestPasswordReset and show success message', async () => { render(); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'forgot@example.com' }, }); fireEvent.submit(screen.getByTestId('reset-password-form')); await waitFor(() => { expect(mockedApiClient.requestPasswordReset).toHaveBeenCalledWith( 'forgot@example.com', expect.any(AbortSignal), ); expect(notifySuccess).toHaveBeenCalledWith('Password reset email sent.'); }); }); it('should display an error on failed password reset request', async () => { (mockedApiClient.requestPasswordReset as Mock).mockRejectedValueOnce( new Error('User not found'), ); render(); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.submit(screen.getByTestId('reset-password-form')); await waitFor(() => { expect(notifyError).toHaveBeenCalledWith('User not found'); }); }); it('should display an error on non-OK password reset response', async () => { (mockedApiClient.requestPasswordReset as Mock).mockResolvedValueOnce( new Response(JSON.stringify({ message: 'Rate limit exceeded' }), { status: 429 }), ); render(); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.submit(screen.getByTestId('reset-password-form')); await waitFor(() => { expect(notifyError).toHaveBeenCalledWith('Rate limit exceeded'); }); }); it('should switch back to sign in from forgot password', () => { render(); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.click(screen.getByRole('button', { name: /back to sign in/i })); expect(screen.getByRole('heading', { name: /sign in/i })).toBeInTheDocument(); }); }); describe('OAuth', () => { const originalLocation = window.location; beforeEach(() => { Object.defineProperty(window, 'location', { writable: true, value: { ...originalLocation, href: '' }, }); }); afterEach(() => { Object.defineProperty(window, 'location', { writable: true, value: originalLocation, }); }); it('should set window.location.href for Google OAuth', () => { render(); fireEvent.click(screen.getByRole('button', { name: /sign in with google/i })); expect(window.location.href).toBe('/api/auth/google'); }); it('should set window.location.href for GitHub OAuth', () => { render(); fireEvent.click(screen.getByRole('button', { name: /sign in with github/i })); expect(window.location.href).toBe('/api/auth/github'); }); }); describe('UI Logic and Loading States', () => { it('should toggle "Remember me" checkbox', () => { render(); const rememberMeCheckbox = screen.getByRole('checkbox', { name: /remember me/i }); expect(rememberMeCheckbox).not.toBeChecked(); fireEvent.click(rememberMeCheckbox); expect(rememberMeCheckbox).toBeChecked(); fireEvent.click(rememberMeCheckbox); expect(rememberMeCheckbox).not.toBeChecked(); }); it('should show loading state during login submission', async () => { // Mock a promise that doesn't resolve immediately (mockedApiClient.loginUser as Mock).mockReturnValue(new Promise(() => {})); render(); fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'test@example.com' }, }); fireEvent.change(screen.getByLabelText(/^password$/i), { target: { value: 'password' } }); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { // Find the submit button. Since the text is replaced by a spinner, we find by type. const submitButton = screen.getByTestId('auth-form').querySelector('button[type="submit"]'); expect(submitButton).toBeInTheDocument(); expect(submitButton).toBeDisabled(); // Verify 'Sign In' text is gone from the button // Note: We use queryByRole because 'Sign In' still exists in the header (h2). expect(screen.queryByRole('button', { name: 'Sign In' })).not.toBeInTheDocument(); // Also check OAuth buttons are disabled expect(screen.getByRole('button', { name: /sign in with google/i })).toBeDisabled(); expect(screen.getByRole('button', { name: /sign in with github/i })).toBeDisabled(); }); }); it('should show loading state during password reset submission', async () => { (mockedApiClient.requestPasswordReset as Mock).mockReturnValue(new Promise(() => {})); render(); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'reset@example.com' }, }); fireEvent.submit(screen.getByTestId('reset-password-form')); const submitButton = screen .getByTestId('reset-password-form') .querySelector('button[type="submit"]'); expect(submitButton).toBeInTheDocument(); expect(submitButton).toBeDisabled(); expect(screen.queryByText('Send Reset Link')).not.toBeInTheDocument(); }); }); it('should show loading state during registration submission', async () => { // Mock a promise that doesn't resolve immediately (mockedApiClient.registerUser as Mock).mockReturnValue(new Promise(() => {})); render(); // Switch to registration view fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'test@example.com' }, }); fireEvent.change(screen.getByTestId('password-input'), { target: { value: 'password' } }); fireEvent.submit(screen.getByTestId('auth-form')); await waitFor(() => { const submitButton = screen.getByTestId('auth-form').querySelector('button[type="submit"]'); expect(submitButton).toBeInTheDocument(); expect(submitButton).toBeDisabled(); // Verify the text 'Register' is gone from any button expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument(); }); }); });