Files
gridpilot.gg/apps/website/tests/flows/auth.test.tsx
Marc Mintel c22e26d14c
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m48s
Contract Testing / contract-snapshot (pull_request) Has been skipped
view data tests
2026-01-22 17:27:08 +01:00

1083 lines
38 KiB
TypeScript

/**
* Auth Feature Flow Tests
*
* These tests verify routing, guards, navigation, cross-screen state, and user flows
* for the auth module. They run with vitest and React Testing Library.
*
* Contracts are defined in apps/website/lib/types/generated
*
* @file apps/website/tests/flows/auth.test.tsx
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { LoginClient } from '@/client-wrapper/LoginClient';
import { SignupClient } from '@/client-wrapper/SignupClient';
import { ForgotPasswordClient } from '@/client-wrapper/ForgotPasswordClient';
import { ResetPasswordClient } from '@/client-wrapper/ResetPasswordClient';
import type { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
import type { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
import type { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
import type { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
import { Result } from '@/lib/contracts/Result';
// Mock next/navigation
const mockPush = vi.fn();
const mockReplace = vi.fn();
const mockRefresh = vi.fn();
const mockSearchParams = new URLSearchParams();
// Mock window.location to prevent navigation errors
const originalLocation = window.location;
delete (window as any).location;
(window as any).location = {
href: '',
pathname: '/auth/login',
search: '',
hash: '',
origin: 'http://localhost:3000',
protocol: 'http:',
host: 'localhost:3000',
hostname: 'localhost',
port: '3000',
assign: vi.fn(),
replace: vi.fn(),
reload: vi.fn(),
};
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
replace: mockReplace,
refresh: mockRefresh,
}),
useSearchParams: () => mockSearchParams,
usePathname: () => '/auth/login',
}));
// Mock AuthContext
const mockRefreshSession = vi.fn(() => Promise.resolve());
let mockSession: any = null;
vi.mock('@/components/auth/AuthContext', () => ({
useAuth: () => ({
refreshSession: mockRefreshSession,
session: mockSession,
}),
}));
// Mock mutations
const mockLoginMutation = {
execute: vi.fn(() => Promise.resolve(Result.ok({}))),
};
const mockSignupMutation = {
execute: vi.fn(() => Promise.resolve(Result.ok({}))),
};
const mockForgotPasswordMutation = {
execute: vi.fn(() => Promise.resolve(Result.ok({}))),
};
const mockResetPasswordMutation = {
execute: vi.fn(() => Promise.resolve(Result.ok({}))),
};
vi.mock('@/lib/mutations/auth/LoginMutation', () => ({
LoginMutation: vi.fn().mockImplementation(() => ({
execute: (...args: any[]) => mockLoginMutation.execute(...args),
})),
}));
vi.mock('@/lib/mutations/auth/SignupMutation', () => ({
SignupMutation: vi.fn().mockImplementation(() => ({
execute: (...args: any[]) => mockSignupMutation.execute(...args),
})),
}));
vi.mock('@/lib/mutations/auth/ForgotPasswordMutation', () => ({
ForgotPasswordMutation: vi.fn().mockImplementation(() => ({
execute: (...args: any[]) => mockForgotPasswordMutation.execute(...args),
})),
}));
vi.mock('@/lib/mutations/auth/ResetPasswordMutation', () => ({
ResetPasswordMutation: vi.fn().mockImplementation(() => ({
execute: (...args: any[]) => mockResetPasswordMutation.execute(...args),
})),
}));
// Mock process.env
const originalNodeEnv = process.env.NODE_ENV;
beforeEach(() => {
process.env.NODE_ENV = 'development';
vi.clearAllMocks();
mockSession = null;
mockSearchParams.delete('returnTo');
mockSearchParams.delete('token');
});
afterEach(() => {
process.env.NODE_ENV = originalNodeEnv;
});
describe('Auth Feature Flow', () => {
describe('Login Flow', () => {
const mockLoginViewData: LoginViewData = {
formState: {
fields: {
email: { value: '', touched: false, error: undefined, validating: false },
password: { value: '', touched: false, error: undefined, validating: false },
rememberMe: { value: false, touched: false, error: undefined, validating: false },
},
isValid: true,
isSubmitting: false,
submitCount: 0,
submitError: undefined,
},
showPassword: false,
showErrorDetails: false,
hasInsufficientPermissions: false,
returnTo: '/dashboard',
isSubmitting: false,
};
it('should display login form with all fields', () => {
render(<LoginClient viewData={mockLoginViewData} />);
expect(screen.getByLabelText(/email address/i)).toBeDefined();
expect(screen.getByLabelText(/password/i)).toBeDefined();
expect(screen.getByLabelText(/keep me signed in/i)).toBeDefined();
expect(screen.getByRole('button', { name: /sign in/i })).toBeDefined();
});
it('should display validation errors for empty fields', async () => {
render(<LoginClient viewData={mockLoginViewData} />);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeDefined();
expect(screen.getByText(/password is required/i)).toBeDefined();
});
});
it('should display validation error for invalid email format', async () => {
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/invalid email/i)).toBeDefined();
});
});
it('should successfully login with valid credentials', async () => {
mockLoginMutation.execute.mockImplementation(() => {
mockSession = { userId: 'user-123' };
return Promise.resolve(Result.ok({
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
}));
});
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockLoginMutation.execute).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'ValidPass123!',
rememberMe: false,
});
});
await waitFor(() => {
expect(mockRefreshSession).toHaveBeenCalled();
});
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/dashboard');
});
});
it('should handle login with remember me option', async () => {
mockLoginMutation.execute.mockImplementation(() => {
mockSession = { userId: 'user-123' };
return Promise.resolve(Result.ok({
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
}));
});
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const rememberMeCheckbox = screen.getByLabelText(/keep me signed in/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(rememberMeCheckbox);
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockLoginMutation.execute).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'ValidPass123!',
rememberMe: true,
});
});
});
it('should handle login errors (invalid credentials)', async () => {
mockLoginMutation.execute.mockResolvedValue(
Result.err('Invalid credentials')
);
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'WrongPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeDefined();
});
// Verify form remains in error state
expect((emailInput as HTMLInputElement).value).toBe('wrong@example.com');
});
it('should handle login errors (server error)', async () => {
mockLoginMutation.execute.mockResolvedValue(
Result.err('Internal Server Error')
);
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeDefined();
});
});
it('should navigate to forgot password from login', () => {
render(<LoginClient viewData={mockLoginViewData} />);
const forgotPasswordLink = screen.getByText(/forgot password/i);
fireEvent.click(forgotPasswordLink);
expect(mockPush).toHaveBeenCalledWith('/auth/forgot-password');
});
it('should navigate to signup from login', () => {
render(<LoginClient viewData={mockLoginViewData} />);
const signupLink = screen.getByText(/create one/i);
fireEvent.click(signupLink);
expect(mockPush).toHaveBeenCalledWith('/auth/signup');
});
it('should handle password visibility toggle', () => {
render(<LoginClient viewData={mockLoginViewData} />);
const passwordInput = screen.getByLabelText(/password/i);
expect((passwordInput as HTMLInputElement).type).toBe('password');
const toggleButton = screen.getByRole('button', { name: /show password/i });
fireEvent.click(toggleButton);
expect((passwordInput as HTMLInputElement).type).toBe('text');
});
});
describe('Signup Flow', () => {
const mockSignupViewData: SignupViewData = {
returnTo: '/onboarding',
formState: {
fields: {
firstName: { value: '', touched: false, error: undefined, validating: false },
lastName: { value: '', touched: false, error: undefined, validating: false },
email: { value: '', touched: false, error: undefined, validating: false },
password: { value: '', touched: false, error: undefined, validating: false },
confirmPassword: { value: '', touched: false, error: undefined, validating: false },
},
isValid: true,
isSubmitting: false,
submitCount: 0,
submitError: undefined,
},
isSubmitting: false,
};
it('should display signup form with all fields', () => {
render(<SignupClient viewData={mockSignupViewData} />);
expect(screen.getByLabelText(/first name/i)).toBeDefined();
expect(screen.getByLabelText(/last name/i)).toBeDefined();
expect(screen.getByLabelText(/email address/i)).toBeDefined();
expect(screen.getByLabelText(/^password$/i)).toBeDefined();
expect(screen.getByLabelText(/confirm password/i)).toBeDefined();
expect(screen.getByRole('button', { name: /create account/i })).toBeDefined();
});
it('should display validation errors for empty required fields', async () => {
render(<SignupClient viewData={mockSignupViewData} />);
const submitButton = screen.getByRole('button', { name: /create account/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/first name is required/i)).toBeDefined();
expect(screen.getByText(/last name is required/i)).toBeDefined();
expect(screen.getByText(/email is required/i)).toBeDefined();
expect(screen.getByText(/password is required/i)).toBeDefined();
expect(screen.getByText(/confirm password is required/i)).toBeDefined();
});
});
it('should display validation errors for weak password', async () => {
render(<SignupClient viewData={mockSignupViewData} />);
const passwordInput = screen.getByLabelText(/^password$/i);
fireEvent.change(passwordInput, { target: { value: 'weak' } });
const submitButton = screen.getByRole('button', { name: /create account/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/password must be/i)).toBeDefined();
});
});
it('should successfully signup with valid data', async () => {
mockSignupMutation.execute.mockImplementation(() => {
mockSession = { userId: 'user-456' };
return Promise.resolve(Result.ok({
token: 'test-token-456',
user: {
userId: 'user-456',
email: 'newuser@example.com',
displayName: 'New User',
role: 'user',
},
}));
});
render(<SignupClient viewData={mockSignupViewData} />);
const firstNameInput = screen.getByLabelText(/first name/i);
const lastNameInput = screen.getByLabelText(/last name/i);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/^password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const submitButton = screen.getByRole('button', { name: /create account/i });
fireEvent.change(firstNameInput, { target: { value: 'New' } });
fireEvent.change(lastNameInput, { target: { value: 'User' } });
fireEvent.change(emailInput, { target: { value: 'newuser@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockSignupMutation.execute).toHaveBeenCalledWith({
email: 'newuser@example.com',
password: 'ValidPass123!',
displayName: 'New User',
});
});
await waitFor(() => {
expect(mockRefreshSession).toHaveBeenCalled();
});
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/onboarding');
});
});
it('should handle signup errors (email already exists)', async () => {
mockSignupMutation.execute.mockResolvedValue(
Result.err('Email already exists')
);
render(<SignupClient viewData={mockSignupViewData} />);
const firstNameInput = screen.getByLabelText(/first name/i);
const lastNameInput = screen.getByLabelText(/last name/i);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/^password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const submitButton = screen.getByRole('button', { name: /create account/i });
fireEvent.change(firstNameInput, { target: { value: 'Existing' } });
fireEvent.change(lastNameInput, { target: { value: 'User' } });
fireEvent.change(emailInput, { target: { value: 'existing@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/already exists/i)).toBeDefined();
});
});
it('should handle signup errors (server error)', async () => {
mockSignupMutation.execute.mockResolvedValue(
Result.err('Internal Server Error')
);
render(<SignupClient viewData={mockSignupViewData} />);
const firstNameInput = screen.getByLabelText(/first name/i);
const lastNameInput = screen.getByLabelText(/last name/i);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/^password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const submitButton = screen.getByRole('button', { name: /create account/i });
fireEvent.change(firstNameInput, { target: { value: 'Test' } });
fireEvent.change(lastNameInput, { target: { value: 'User' } });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeDefined();
});
});
it('should navigate to login from signup', () => {
render(<SignupClient viewData={mockSignupViewData} />);
const loginLink = screen.getByText(/already have an account/i);
fireEvent.click(loginLink);
expect(mockPush).toHaveBeenCalledWith('/auth/login');
});
it('should handle password visibility toggle', () => {
render(<SignupClient viewData={mockSignupViewData} />);
const passwordInput = screen.getByLabelText(/^password$/i);
expect((passwordInput as HTMLInputElement).type).toBe('password');
const toggleButton = screen.getByRole('button', { name: /show password/i });
fireEvent.click(toggleButton);
expect((passwordInput as HTMLInputElement).type).toBe('text');
});
it('should handle confirm password visibility toggle', () => {
render(<SignupClient viewData={mockSignupViewData} />);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
expect((confirmPasswordInput as HTMLInputElement).type).toBe('password');
const toggleButton = screen.getByRole('button', { name: /show confirm password/i });
fireEvent.click(toggleButton);
expect((confirmPasswordInput as HTMLInputElement).type).toBe('text');
});
});
describe('Forgot Password Flow', () => {
const mockForgotPasswordViewData: ForgotPasswordViewData = {
returnTo: '/auth/login',
formState: {
fields: {
email: { value: '', touched: false, error: undefined, validating: false },
},
isValid: true,
isSubmitting: false,
submitCount: 0,
submitError: undefined,
},
showSuccess: false,
successMessage: undefined,
magicLink: undefined,
isSubmitting: false,
};
it('should display forgot password form with email field', () => {
render(<ForgotPasswordClient viewData={mockForgotPasswordViewData} />);
expect(screen.getByLabelText(/email address/i)).toBeDefined();
expect(screen.getByRole('button', { name: /send reset link/i })).toBeDefined();
});
it('should display validation error for empty email', async () => {
render(<ForgotPasswordClient viewData={mockForgotPasswordViewData} />);
const submitButton = screen.getByRole('button', { name: /send reset link/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeDefined();
});
});
it('should display validation error for invalid email format', async () => {
render(<ForgotPasswordClient viewData={mockForgotPasswordViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
const submitButton = screen.getByRole('button', { name: /send reset link/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/invalid email/i)).toBeDefined();
});
});
it('should successfully submit forgot password request', async () => {
mockForgotPasswordMutation.execute.mockResolvedValue(
Result.ok({
success: true,
message: 'Password reset email sent',
})
);
render(<ForgotPasswordClient viewData={mockForgotPasswordViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const submitButton = screen.getByRole('button', { name: /send reset link/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockForgotPasswordMutation.execute).toHaveBeenCalledWith({
email: 'test@example.com',
});
});
await waitFor(() => {
expect(screen.getByText(/password reset email sent/i)).toBeDefined();
});
});
it('should handle forgot password errors (email not found)', async () => {
mockForgotPasswordMutation.execute.mockResolvedValue(
Result.err('Email not found')
);
render(<ForgotPasswordClient viewData={mockForgotPasswordViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const submitButton = screen.getByRole('button', { name: /send reset link/i });
fireEvent.change(emailInput, { target: { value: 'nonexistent@example.com' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/email not found/i)).toBeDefined();
});
});
it('should handle forgot password errors (rate limit)', async () => {
mockForgotPasswordMutation.execute.mockResolvedValue(
Result.err('Rate limit exceeded')
);
render(<ForgotPasswordClient viewData={mockForgotPasswordViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const submitButton = screen.getByRole('button', { name: /send reset link/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/rate limit/i)).toBeDefined();
});
});
it('should navigate back to login from forgot password', () => {
render(<ForgotPasswordClient viewData={mockForgotPasswordViewData} />);
const loginLink = screen.getByText(/back to login/i);
fireEvent.click(loginLink);
expect(mockPush).toHaveBeenCalledWith('/auth/login');
});
});
describe('Reset Password Flow', () => {
const mockResetPasswordViewData: ResetPasswordViewData = {
token: 'abc123',
returnTo: '/auth/login',
formState: {
fields: {
newPassword: { value: '', touched: false, error: undefined, validating: false },
confirmPassword: { value: '', touched: false, error: undefined, validating: false },
},
isValid: true,
isSubmitting: false,
submitCount: 0,
submitError: undefined,
},
showSuccess: false,
successMessage: undefined,
isSubmitting: false,
};
beforeEach(() => {
mockSearchParams.set('token', 'abc123');
});
it('should display reset password form with password fields', () => {
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
expect(screen.getByLabelText(/^new password$/i)).toBeDefined();
expect(screen.getByLabelText(/confirm password/i)).toBeDefined();
expect(screen.getByRole('button', { name: /reset password/i })).toBeDefined();
});
it('should display validation errors for empty password fields', async () => {
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const submitButton = screen.getByRole('button', { name: /reset password/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/new password is required/i)).toBeDefined();
expect(screen.getByText(/confirm password is required/i)).toBeDefined();
});
});
it('should display validation error for non-matching passwords', async () => {
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
fireEvent.change(newPasswordInput, { target: { value: 'ValidPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPass456!' } });
const submitButton = screen.getByRole('button', { name: /reset password/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/passwords do not match/i)).toBeDefined();
});
});
it('should display validation error for weak new password', async () => {
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
fireEvent.change(newPasswordInput, { target: { value: 'weak' } });
const submitButton = screen.getByRole('button', { name: /reset password/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/password must be/i)).toBeDefined();
});
});
it('should successfully reset password', async () => {
mockResetPasswordMutation.execute.mockResolvedValue(
Result.ok({
success: true,
message: 'Password reset successfully',
})
);
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const submitButton = screen.getByRole('button', { name: /reset password/i });
fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockResetPasswordMutation.execute).toHaveBeenCalledWith({
token: 'abc123',
newPassword: 'NewPass123!',
});
});
await waitFor(() => {
expect(screen.getByText(/password reset successfully/i)).toBeDefined();
});
// Verify redirect to login page after delay
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/auth/login');
}, { timeout: 5000 });
});
it('should handle reset password with invalid token', async () => {
mockResetPasswordMutation.execute.mockResolvedValue(
Result.err('Invalid token')
);
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const submitButton = screen.getByRole('button', { name: /reset password/i });
fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/invalid token/i)).toBeDefined();
});
// Verify form is disabled
expect((submitButton as HTMLButtonElement).disabled).toBe(true);
});
it('should handle reset password with expired token', async () => {
mockResetPasswordMutation.execute.mockResolvedValue(
Result.err('Token expired')
);
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const submitButton = screen.getByRole('button', { name: /reset password/i });
fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/token expired/i)).toBeDefined();
});
// Verify link to request new reset email
expect(screen.getByText(/request new link/i)).toBeDefined();
});
it('should handle reset password errors (server error)', async () => {
mockResetPasswordMutation.execute.mockResolvedValue(
Result.err('Internal Server Error')
);
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
const submitButton = screen.getByRole('button', { name: /reset password/i });
fireEvent.change(newPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'NewPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeDefined();
});
});
it('should navigate to login from reset password', () => {
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const loginLink = screen.getByText(/back to login/i);
fireEvent.click(loginLink);
expect(mockPush).toHaveBeenCalledWith('/auth/login');
});
it('should handle password visibility toggle', () => {
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
expect((newPasswordInput as HTMLInputElement).type).toBe('password');
const toggleButton = screen.getByRole('button', { name: /show password/i });
fireEvent.click(toggleButton);
expect((newPasswordInput as HTMLInputElement).type).toBe('text');
});
it('should handle confirm password visibility toggle', () => {
render(<ResetPasswordClient viewData={mockResetPasswordViewData} />);
const confirmPasswordInput = screen.getByLabelText(/confirm password/i);
expect((confirmPasswordInput as HTMLInputElement).type).toBe('password');
const toggleButton = screen.getByRole('button', { name: /show confirm password/i });
fireEvent.click(toggleButton);
expect((confirmPasswordInput as HTMLInputElement).type).toBe('text');
});
});
describe('Auth Cross-Screen State Management', () => {
const mockLoginViewData: LoginViewData = {
formState: {
fields: {
email: { value: '', touched: false, error: undefined, validating: false },
password: { value: '', touched: false, error: undefined, validating: false },
rememberMe: { value: false, touched: false, error: undefined, validating: false },
},
isValid: true,
isSubmitting: false,
submitCount: 0,
submitError: undefined,
},
showPassword: false,
showErrorDetails: false,
hasInsufficientPermissions: false,
returnTo: '/dashboard',
isSubmitting: false,
};
it('should preserve form data when navigating between auth pages', async () => {
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
// Navigate to forgot password
const forgotPasswordLink = screen.getByText(/forgot password/i);
fireEvent.click(forgotPasswordLink);
// Navigate back to login
const loginLink = screen.getByText(/back to login/i);
fireEvent.click(loginLink);
// Verify email is preserved
await waitFor(() => {
expect((emailInput as HTMLInputElement).value).toBe('test@example.com');
});
});
it('should clear form data after successful authentication', async () => {
mockLoginMutation.execute.mockImplementation(() => {
mockSession = { userId: 'user-123' };
return Promise.resolve(Result.ok({
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
}));
});
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/dashboard');
});
// Navigate back to login
mockPush.mockClear();
const loginLink = screen.getByText(/sign in/i);
fireEvent.click(loginLink);
// Verify form is cleared
await waitFor(() => {
expect((emailInput as HTMLInputElement).value).toBe('');
expect((passwordInput as HTMLInputElement).value).toBe('');
});
});
it('should handle concurrent auth operations', async () => {
mockLoginMutation.execute.mockImplementation(() => {
mockSession = { userId: 'user-123' };
return Promise.resolve(Result.ok({
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
}));
});
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
// Click submit multiple times quickly
fireEvent.click(submitButton);
fireEvent.click(submitButton);
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockLoginMutation.execute).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/dashboard');
});
});
});
describe('Auth UI State Management', () => {
const mockLoginViewData: LoginViewData = {
formState: {
fields: {
email: { value: '', touched: false, error: undefined, validating: false },
password: { value: '', touched: false, error: undefined, validating: false },
rememberMe: { value: false, touched: false, error: undefined, validating: false },
},
isValid: true,
isSubmitting: false,
submitCount: 0,
submitError: undefined,
},
showPassword: false,
showErrorDetails: false,
hasInsufficientPermissions: false,
returnTo: '/dashboard',
isSubmitting: false,
};
it('should show loading states during auth operations', async () => {
mockLoginMutation.execute.mockImplementation(
() => new Promise(resolve => setTimeout(() => {
mockSession = { userId: 'user-123' };
resolve(Result.ok({
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
}));
}, 100))
);
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
// Verify loading state
await waitFor(() => {
expect(screen.getByText(/signing in/i)).toBeDefined();
});
// Wait for loading to complete
await waitFor(() => {
expect(screen.queryByText(/signing in/i)).toBeNull();
});
// Verify authentication is successful
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/dashboard');
});
});
it('should handle error states gracefully', async () => {
mockLoginMutation.execute.mockResolvedValue(
Result.err('Invalid credentials')
);
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'WrongPass123!' } });
fireEvent.click(submitButton);
// Verify error message is displayed
await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeDefined();
});
// Verify UI remains usable after errors
expect((emailInput as HTMLInputElement).disabled).toBe(false);
expect((passwordInput as HTMLInputElement).disabled).toBe(false);
expect((submitButton as HTMLButtonElement).disabled).toBe(false);
});
it('should handle network connectivity issues', async () => {
mockLoginMutation.execute.mockResolvedValue(
Result.err('Network Error')
);
render(<LoginClient viewData={mockLoginViewData} />);
const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } });
fireEvent.click(submitButton);
// Verify network error message is shown
await waitFor(() => {
expect(screen.getByText(/network/i)).toBeDefined();
});
});
});
});