test: vitest setup + auth/login/cookie banner tests

This commit is contained in:
neckfire
2026-06-20 12:57:13 +02:00
parent 7a4206a522
commit bfdb3d3d9a
8 changed files with 996 additions and 6 deletions
+9 -2
View File
@@ -7,7 +7,9 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"lucide-react": "^1.8.0",
@@ -25,6 +27,11 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"vite": "^8.0.10"
"vite": "^8.0.10",
"vitest": "^4.1.9",
"jsdom": "^29.1.1",
"@testing-library/react": "^16.3.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/user-event": "^14.6.1"
}
}
+753 -4
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
/* Tests du contexte d'authentification : login / logout */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { AuthProvider, useAuth } from '../context/AuthContext';
import * as api from '../services/api';
describe('AuthContext', () => {
beforeEach(() => {
localStorage.clear();
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
function wrapper({ children }) {
return <AuthProvider>{children}</AuthProvider>;
}
it('login renseigne l\'utilisateur, logout le vide', async () => {
const fakeUser = { username: 'bob', role: 'Consultant' };
vi.spyOn(api, 'login').mockResolvedValue(fakeUser);
const clearSpy = vi.spyOn(api, 'clearToken').mockImplementation(() => {});
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
await act(async () => {
await result.current.login('bob', 'pwd');
});
expect(result.current.user).toEqual(fakeUser);
expect(result.current.isAuthenticated).toBe(true);
/* L'utilisateur est persisté dans le localStorage */
expect(JSON.parse(localStorage.getItem('ds_user'))).toEqual(fakeUser);
act(() => {
result.current.logout();
});
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
expect(localStorage.getItem('ds_user')).toBeNull();
expect(clearSpy).toHaveBeenCalled();
});
});
@@ -0,0 +1,47 @@
/* Tests du bandeau de consentement aux cookies (RGPD) */
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import CookieBanner from '../components/common/CookieBanner';
function renderBanner() {
return render(
<MemoryRouter>
<CookieBanner />
</MemoryRouter>
);
}
describe('CookieBanner', () => {
beforeEach(() => {
localStorage.clear();
});
it('s\'affiche tant qu\'aucun consentement n\'est enregistré', () => {
renderBanner();
expect(screen.getByRole('dialog', { name: /Consentement aux cookies/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Tout accepter' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Refuser' })).toBeInTheDocument();
});
it('se masque et persiste le choix après "Tout accepter"', () => {
renderBanner();
fireEvent.click(screen.getByRole('button', { name: 'Tout accepter' }));
/* Le bandeau disparaît */
expect(screen.queryByRole('dialog', { name: /Consentement aux cookies/ })).not.toBeInTheDocument();
/* La décision est persistée au format attendu */
const stored = JSON.parse(localStorage.getItem('ds_cookies_consent'));
expect(stored.choice).toBe('accept');
expect(stored.version).toBe(1);
expect(typeof stored.date).toBe('string');
});
it('ne s\'affiche pas si un consentement existe déjà', () => {
localStorage.setItem('ds_cookies_consent', JSON.stringify({ choice: 'refuse', date: '2026-01-01', version: 1 }));
renderBanner();
expect(screen.queryByRole('dialog', { name: /Consentement aux cookies/ })).not.toBeInTheDocument();
});
});
+44
View File
@@ -0,0 +1,44 @@
/* Tests de la page de connexion : validation du captcha anti-bot */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import Login from '../pages/Login/Login';
import { AuthProvider } from '../context/AuthContext';
import * as api from '../services/api';
function renderLogin() {
return render(
<MemoryRouter>
<AuthProvider>
<Login />
</AuthProvider>
</MemoryRouter>
);
}
describe('Login - captcha', () => {
beforeEach(() => {
localStorage.clear();
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('affiche une erreur et n\'appelle pas l\'API si le captcha est faux', async () => {
const loginSpy = vi.spyOn(api, 'login').mockResolvedValue({ username: 'x', role: 'Admin' });
renderLogin();
fireEvent.change(screen.getByLabelText('Identifiant'), { target: { value: 'alice' } });
fireEvent.change(screen.getByLabelText('Mot de passe'), { target: { value: 'pwd' } });
/* Réponse volontairement absurde, jamais égale à x + y (max 20) */
fireEvent.change(screen.getByLabelText(/combien font/i), { target: { value: '9999' } });
fireEvent.click(screen.getByRole('button', { name: /Se connecter/ }));
expect(await screen.findByText('Réponse au captcha incorrecte.')).toBeInTheDocument();
expect(loginSpy).not.toHaveBeenCalled();
});
});
+82
View File
@@ -0,0 +1,82 @@
/* Tests du service API : authentification et gestion du token JWT */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { login, getServices, getToken, setToken } from '../services/api';
describe('api.login', () => {
beforeEach(() => {
localStorage.clear();
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('stocke le token et envoie un corps urlencoded', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
access_token: 'jwt-abc-123',
token_type: 'bearer',
user: { username: 'alice', role: 'Admin' },
}),
});
vi.stubGlobal('fetch', fetchMock);
const user = await login('alice', 's3cret');
/* Le token est persisté */
expect(getToken()).toBe('jwt-abc-123');
/* L'utilisateur retourné provient de la réponse */
expect(user).toEqual({ username: 'alice', role: 'Admin' });
/* Vérifie l'URL, la méthode et l'en-tête de contenu */
const [url, options] = fetchMock.mock.calls[0];
expect(url).toMatch(/\/auth\/login$/);
expect(options.method).toBe('POST');
expect(options.headers['Content-Type']).toBe('application/x-www-form-urlencoded');
/* Le corps urlencoded contient bien username et password */
const params = new URLSearchParams(options.body);
expect(params.get('username')).toBe('alice');
expect(params.get('password')).toBe('s3cret');
});
it('lève "Identifiants invalides" sur une réponse 401', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: false,
status: 401,
text: async () => 'Unauthorized',
}));
await expect(login('alice', 'wrong')).rejects.toThrow('Identifiants invalides');
expect(getToken()).toBeNull();
});
});
describe('fetchApi (401)', () => {
beforeEach(() => {
localStorage.clear();
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('purge le token lorsqu\'une requête renvoie 401', async () => {
setToken('expired-token');
expect(getToken()).toBe('expired-token');
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: false,
status: 401,
text: async () => 'Unauthorized',
}));
await expect(getServices()).rejects.toThrow();
/* Le token doit avoir été supprimé par handleUnauthorized */
expect(getToken()).toBeNull();
});
});
+7
View File
@@ -0,0 +1,7 @@
/* Configuration globale des tests : matchers jest-dom + nettoyage du localStorage */
import '@testing-library/jest-dom';
import { afterEach } from 'vitest';
afterEach(() => {
localStorage.clear();
});
+5
View File
@@ -6,4 +6,9 @@ export default defineConfig({
plugins: [
react(),
],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.js',
},
})