- Consolidate API logic: hooks/useApi.ts now uses services/api.ts - Create BaseFormField component to reduce form duplication - Refactor FormField, FormSelect, FormTextarea to use BaseFormField - Add centralized theme utility (utils/theme.ts) for colors/styles - Add comprehensive tests for api, auth.service, useApi hooks, AuthContext - Add tests for theme utility This reduces duplication from 11.45% and improves test coverage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
438 lines
11 KiB
TypeScript
438 lines
11 KiB
TypeScript
/**
|
|
* Tests for the AuthContext
|
|
* @jest-environment jsdom
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
|
import { AuthProvider, useAuth } from '@/contexts/AuthContext';
|
|
import { authService } from '@/services/auth.service';
|
|
import toast from 'react-hot-toast';
|
|
|
|
// Mock authService
|
|
jest.mock('@/services/auth.service', () => ({
|
|
authService: {
|
|
login: jest.fn(),
|
|
register: jest.fn(),
|
|
logout: jest.fn(),
|
|
getCurrentUser: jest.fn(),
|
|
googleLogin: jest.fn(),
|
|
facebookLogin: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock react-hot-toast
|
|
jest.mock('react-hot-toast', () => ({
|
|
success: jest.fn(),
|
|
error: jest.fn(),
|
|
}));
|
|
|
|
// Mock utils/helpers
|
|
jest.mock('@/utils/helpers', () => ({
|
|
setToken: jest.fn(),
|
|
removeToken: jest.fn(),
|
|
getToken: jest.fn(),
|
|
storage: {
|
|
get: jest.fn(),
|
|
set: jest.fn(),
|
|
remove: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
import { setToken, removeToken, getToken, storage } from '@/utils/helpers';
|
|
|
|
// Mock next/navigation
|
|
const mockPush = jest.fn();
|
|
const mockReplace = jest.fn();
|
|
jest.mock('next/navigation', () => ({
|
|
useRouter: () => ({
|
|
push: mockPush,
|
|
replace: mockReplace,
|
|
prefetch: jest.fn(),
|
|
}),
|
|
}));
|
|
|
|
// Test component that uses the hook
|
|
const TestComponent: React.FC = () => {
|
|
const { user, isLoading, isAuthenticated, login, logout, register } = useAuth();
|
|
|
|
if (isLoading) return <div>Loading...</div>;
|
|
|
|
const handleLogin = async () => {
|
|
try {
|
|
await login({ email: 'test@test.com', password: 'password' });
|
|
} catch {
|
|
// Error is handled in AuthContext
|
|
}
|
|
};
|
|
|
|
const handleRegister = async () => {
|
|
try {
|
|
await register({
|
|
email: 'new@test.com',
|
|
password: 'password',
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
});
|
|
} catch {
|
|
// Error is handled in AuthContext
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div data-testid="authenticated">{isAuthenticated ? 'yes' : 'no'}</div>
|
|
<div data-testid="user">{user ? JSON.stringify(user) : 'null'}</div>
|
|
<button onClick={handleLogin}>Login</button>
|
|
<button onClick={() => logout()}>Logout</button>
|
|
<button onClick={handleRegister}>Register</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
describe('AuthContext', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
(getToken as jest.Mock).mockReturnValue(null);
|
|
});
|
|
|
|
describe('Initial state', () => {
|
|
it('should show loading state when token exists', async () => {
|
|
// When token exists, it fetches user data which shows loading
|
|
(getToken as jest.Mock).mockReturnValue('valid-token');
|
|
|
|
// Delay the response to ensure loading state is visible
|
|
let resolveUser: (value: unknown) => void;
|
|
const userPromise = new Promise((resolve) => {
|
|
resolveUser = resolve;
|
|
});
|
|
(authService.getCurrentUser as jest.Mock).mockReturnValue(userPromise);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
// Should show loading while fetching user
|
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
|
|
// Resolve the promise
|
|
await act(async () => {
|
|
resolveUser!({ id: '1', email: 'test@test.com', role: 'CLIENT' });
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should have null user when no token exists', async () => {
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
// When no token, loading completes immediately
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('user')).toHaveTextContent('null');
|
|
expect(screen.getByTestId('authenticated')).toHaveTextContent('no');
|
|
});
|
|
});
|
|
|
|
it('should load user when token exists', async () => {
|
|
const mockUser = {
|
|
id: '1',
|
|
email: 'test@test.com',
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
role: 'CLIENT',
|
|
};
|
|
|
|
(getToken as jest.Mock).mockReturnValue('valid-token');
|
|
(authService.getCurrentUser as jest.Mock).mockResolvedValueOnce(mockUser);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
|
|
});
|
|
});
|
|
|
|
it('should remove token if getCurrentUser fails', async () => {
|
|
(getToken as jest.Mock).mockReturnValue('invalid-token');
|
|
(authService.getCurrentUser as jest.Mock).mockRejectedValueOnce(
|
|
new Error('Invalid token')
|
|
);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(removeToken).toHaveBeenCalled();
|
|
expect(storage.remove).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('login', () => {
|
|
it('should login successfully and redirect CLIENT', async () => {
|
|
const mockResponse = {
|
|
token: 'new-token',
|
|
user: {
|
|
id: '1',
|
|
email: 'test@test.com',
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
role: 'CLIENT',
|
|
},
|
|
};
|
|
|
|
(authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
const loginButton = screen.getByText('Login');
|
|
await act(async () => {
|
|
fireEvent.click(loginButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(setToken).toHaveBeenCalledWith('new-token');
|
|
expect(toast.success).toHaveBeenCalledWith('Connexion réussie !');
|
|
expect(mockReplace).toHaveBeenCalledWith('/client');
|
|
});
|
|
});
|
|
|
|
it('should redirect ADMIN to admin dashboard', async () => {
|
|
const mockResponse = {
|
|
token: 'admin-token',
|
|
user: {
|
|
id: '1',
|
|
email: 'admin@test.com',
|
|
firstName: 'Admin',
|
|
lastName: 'User',
|
|
role: 'ADMIN',
|
|
},
|
|
};
|
|
|
|
(authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
const loginButton = screen.getByText('Login');
|
|
await act(async () => {
|
|
fireEvent.click(loginButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockReplace).toHaveBeenCalledWith('/admin/dashboard');
|
|
});
|
|
});
|
|
|
|
it('should redirect EMPLOYEE to employee dashboard', async () => {
|
|
const mockResponse = {
|
|
token: 'employee-token',
|
|
user: {
|
|
id: '1',
|
|
email: 'employee@test.com',
|
|
firstName: 'Employee',
|
|
lastName: 'User',
|
|
role: 'EMPLOYEE',
|
|
},
|
|
};
|
|
|
|
(authService.login as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
const loginButton = screen.getByText('Login');
|
|
await act(async () => {
|
|
fireEvent.click(loginButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockReplace).toHaveBeenCalledWith('/employe/dashboard');
|
|
});
|
|
});
|
|
|
|
it('should show error toast on login failure', async () => {
|
|
const errorMessage = 'Invalid credentials';
|
|
(authService.login as jest.Mock).mockRejectedValueOnce(
|
|
new Error(errorMessage)
|
|
);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
const loginButton = screen.getByText('Login');
|
|
await act(async () => {
|
|
fireEvent.click(loginButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('register', () => {
|
|
it('should register successfully', async () => {
|
|
const mockResponse = {
|
|
token: 'new-user-token',
|
|
user: {
|
|
id: '2',
|
|
email: 'new@test.com',
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
role: 'CLIENT',
|
|
},
|
|
};
|
|
|
|
(authService.register as jest.Mock).mockResolvedValueOnce(mockResponse);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
const registerButton = screen.getByText('Register');
|
|
await act(async () => {
|
|
fireEvent.click(registerButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(setToken).toHaveBeenCalledWith('new-user-token');
|
|
expect(toast.success).toHaveBeenCalledWith('Inscription réussie !');
|
|
expect(mockPush).toHaveBeenCalledWith('/client');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('logout', () => {
|
|
it('should logout and redirect to login page', async () => {
|
|
const mockUser = {
|
|
id: '1',
|
|
email: 'test@test.com',
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
role: 'CLIENT',
|
|
};
|
|
|
|
(getToken as jest.Mock).mockReturnValue('valid-token');
|
|
(authService.getCurrentUser as jest.Mock).mockResolvedValueOnce(mockUser);
|
|
(authService.logout as jest.Mock).mockResolvedValueOnce(undefined);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
|
|
});
|
|
|
|
const logoutButton = screen.getByText('Logout');
|
|
await act(async () => {
|
|
fireEvent.click(logoutButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(removeToken).toHaveBeenCalled();
|
|
expect(storage.remove).toHaveBeenCalled();
|
|
expect(toast.success).toHaveBeenCalledWith('Déconnexion réussie');
|
|
expect(mockPush).toHaveBeenCalledWith('/login');
|
|
});
|
|
});
|
|
|
|
it('should still logout even if API call fails', async () => {
|
|
(getToken as jest.Mock).mockReturnValue('valid-token');
|
|
(authService.getCurrentUser as jest.Mock).mockResolvedValueOnce({
|
|
id: '1',
|
|
email: 'test@test.com',
|
|
role: 'CLIENT',
|
|
});
|
|
(authService.logout as jest.Mock).mockRejectedValueOnce(
|
|
new Error('Logout failed')
|
|
);
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('authenticated')).toHaveTextContent('yes');
|
|
});
|
|
|
|
const logoutButton = screen.getByText('Logout');
|
|
await act(async () => {
|
|
fireEvent.click(logoutButton);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(removeToken).toHaveBeenCalled();
|
|
expect(mockPush).toHaveBeenCalledWith('/login');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('useAuth hook error', () => {
|
|
it('should throw error when used outside AuthProvider', () => {
|
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
expect(() => {
|
|
render(<TestComponent />);
|
|
}).toThrow('useAuth must be used within an AuthProvider');
|
|
|
|
consoleError.mockRestore();
|
|
});
|
|
});
|
|
});
|