test
This commit is contained in:
1144
frontend/package-lock.json
generated
1144
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
@@ -17,6 +19,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
@@ -24,7 +29,9 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"vite": "^8.0.12"
|
"vite": "^8.0.12",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/src/setupTests.js
Normal file
8
frontend/src/setupTests.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { afterEach } from 'vitest'
|
||||||
|
import { cleanup } from '@testing-library/react'
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
84
frontend/src/test/fonctionnel/Connexion.test.jsx
Normal file
84
frontend/src/test/fonctionnel/Connexion.test.jsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import Connexion from '../../pages/Connexion'
|
||||||
|
import { renderWithProviders } from '../renderWithProviders'
|
||||||
|
import { loginApi } from '../../services/auth'
|
||||||
|
|
||||||
|
// On mocke le service d'API : les tests de composant ne font pas de vraies requêtes HTTP.
|
||||||
|
vi.mock('../../services/auth', () => ({
|
||||||
|
loginApi: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Connexion', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche le formulaire de connexion', () => {
|
||||||
|
renderWithProviders(<Connexion />, { route: '/connexion' })
|
||||||
|
|
||||||
|
expect(screen.getByRole('heading', { name: /se connecter/i })).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText(/votre email/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText(/votre mot de passe/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /se connecter/i })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche le message de succès après une inscription', () => {
|
||||||
|
renderWithProviders(<Connexion />, {
|
||||||
|
route: { pathname: '/connexion', state: { registered: true } },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText(/compte créé avec succès/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("affiche l'erreur renvoyée par l'API en cas d'échec", async () => {
|
||||||
|
loginApi.mockRejectedValue(new Error('Email ou mot de passe incorrect.'))
|
||||||
|
|
||||||
|
renderWithProviders(<Connexion />, { route: '/connexion' })
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'test@example.com')
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'mauvais-mdp')
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
|
||||||
|
|
||||||
|
expect(await screen.findByText('Email ou mot de passe incorrect.')).toBeInTheDocument()
|
||||||
|
expect(loginApi).toHaveBeenCalledWith({ email: 'test@example.com', password: 'mauvais-mdp' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('connecte un client et redirige vers /mes-gains', async () => {
|
||||||
|
loginApi.mockResolvedValue({
|
||||||
|
token: 'jwt-token',
|
||||||
|
user: { id: 1, email: 'client@example.com', role: 'CLIENT' },
|
||||||
|
})
|
||||||
|
|
||||||
|
renderWithProviders(<Connexion />, {
|
||||||
|
route: '/connexion',
|
||||||
|
routes: { '/mes-gains': <p>Page Mes Gains</p> },
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'client@example.com')
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!')
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
|
||||||
|
|
||||||
|
expect(await screen.findByText('Page Mes Gains')).toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('token')).toBe('jwt-token')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("connecte un admin et redirige vers /admin", async () => {
|
||||||
|
loginApi.mockResolvedValue({
|
||||||
|
token: 'jwt-token',
|
||||||
|
user: { id: 2, email: 'admin@example.com', role: 'ADMIN' },
|
||||||
|
})
|
||||||
|
|
||||||
|
renderWithProviders(<Connexion />, {
|
||||||
|
route: '/connexion',
|
||||||
|
routes: { '/admin': <p>Page Admin</p> },
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'admin@example.com')
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!')
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /se connecter/i }))
|
||||||
|
|
||||||
|
expect(await screen.findByText('Page Admin')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
39
frontend/src/test/fonctionnel/CookieBanner.test.jsx
Normal file
39
frontend/src/test/fonctionnel/CookieBanner.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import CookieBanner from '../../components/CookieBanner'
|
||||||
|
import { renderWithProviders } from '../renderWithProviders'
|
||||||
|
|
||||||
|
describe('CookieBanner', () => {
|
||||||
|
it("s'affiche quand aucun choix n'a été fait", () => {
|
||||||
|
renderWithProviders(<CookieBanner />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/ce site utilise des cookies/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enregistre le consentement et disparaît quand on accepte', async () => {
|
||||||
|
renderWithProviders(<CookieBanner />)
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /accepter/i }))
|
||||||
|
|
||||||
|
expect(screen.queryByText(/ce site utilise des cookies/i)).not.toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('cookieConsent')).toBe('accepted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('enregistre le refus et disparaît quand on refuse', async () => {
|
||||||
|
renderWithProviders(<CookieBanner />)
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /refuser/i }))
|
||||||
|
|
||||||
|
expect(screen.queryByText(/ce site utilise des cookies/i)).not.toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('cookieConsent')).toBe('refused')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ne s'affiche pas si un choix a déjà été fait", () => {
|
||||||
|
localStorage.setItem('cookieConsent', 'accepted')
|
||||||
|
|
||||||
|
renderWithProviders(<CookieBanner />)
|
||||||
|
|
||||||
|
expect(screen.queryByText(/ce site utilise des cookies/i)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
120
frontend/src/test/fonctionnel/Inscription.test.jsx
Normal file
120
frontend/src/test/fonctionnel/Inscription.test.jsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import Inscription from '../../pages/Inscription'
|
||||||
|
import { renderWithProviders } from '../renderWithProviders'
|
||||||
|
import { registerApi, verifyCodeApi } from '../../services/auth'
|
||||||
|
|
||||||
|
vi.mock('../../services/auth', () => ({
|
||||||
|
registerApi: vi.fn(),
|
||||||
|
verifyCodeApi: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Remplit et soumet le formulaire d'inscription (étape 1).
|
||||||
|
async function remplirEtSoumettre() {
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre email/i), 'jean@example.com')
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre nom/i), 'Dupont')
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre prénom/i), 'Jean')
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre mot de passe/i), 'MotDePasse123!')
|
||||||
|
await userEvent.click(screen.getByRole('checkbox'))
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /s'inscrire/i }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saisit le code à 4 chiffres de l'étape de vérification.
|
||||||
|
async function saisirCode(code) {
|
||||||
|
const inputs = screen.getAllByRole('textbox')
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
await userEvent.type(inputs[i], code[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Inscription', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("affiche le formulaire d'inscription", () => {
|
||||||
|
renderWithProviders(<Inscription />, { route: '/inscription' })
|
||||||
|
|
||||||
|
expect(screen.getByRole('heading', { name: /s'inscrire/i })).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText(/votre email/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText(/votre nom/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText(/votre prénom/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText(/votre mot de passe/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("désactive le bouton tant que les conditions ne sont pas acceptées", async () => {
|
||||||
|
renderWithProviders(<Inscription />, { route: '/inscription' })
|
||||||
|
|
||||||
|
const bouton = screen.getByRole('button', { name: /s'inscrire/i })
|
||||||
|
expect(bouton).toBeDisabled()
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('checkbox'))
|
||||||
|
expect(bouton).toBeEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passe à l'étape de vérification après une inscription réussie", async () => {
|
||||||
|
registerApi.mockResolvedValue({ message: 'ok' })
|
||||||
|
|
||||||
|
renderWithProviders(<Inscription />, { route: '/inscription' })
|
||||||
|
await remplirEtSoumettre()
|
||||||
|
|
||||||
|
expect(registerApi).toHaveBeenCalledWith({
|
||||||
|
email: 'jean@example.com',
|
||||||
|
firstname: 'Jean',
|
||||||
|
lastname: 'Dupont',
|
||||||
|
password: 'MotDePasse123!',
|
||||||
|
})
|
||||||
|
expect(await screen.findByRole('heading', { name: /vérifiez votre email/i })).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('jean@example.com')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("affiche l'erreur de l'API si l'inscription échoue", async () => {
|
||||||
|
registerApi.mockRejectedValue(new Error('Cet email est déjà utilisé.'))
|
||||||
|
|
||||||
|
renderWithProviders(<Inscription />, { route: '/inscription' })
|
||||||
|
await remplirEtSoumettre()
|
||||||
|
|
||||||
|
expect(await screen.findByText('Cet email est déjà utilisé.')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('heading', { name: /vérifiez votre email/i })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('connecte et redirige vers /saisir-code-unique quand le code est valide', async () => {
|
||||||
|
registerApi.mockResolvedValue({ message: 'ok' })
|
||||||
|
verifyCodeApi.mockResolvedValue({
|
||||||
|
token: 'jwt-token',
|
||||||
|
user: { id: 1, email: 'jean@example.com', role: 'CLIENT' },
|
||||||
|
})
|
||||||
|
|
||||||
|
renderWithProviders(<Inscription />, {
|
||||||
|
route: '/inscription',
|
||||||
|
routes: { '/saisir-code-unique': <p>Page Saisir Code</p> },
|
||||||
|
})
|
||||||
|
await remplirEtSoumettre()
|
||||||
|
await screen.findByRole('heading', { name: /vérifiez votre email/i })
|
||||||
|
|
||||||
|
await saisirCode('1234')
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /valider le code/i }))
|
||||||
|
|
||||||
|
expect(verifyCodeApi).toHaveBeenCalledWith({ email: 'jean@example.com', code: '1234' })
|
||||||
|
expect(await screen.findByText('Page Saisir Code')).toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('token')).toBe('jwt-token')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("affiche l'erreur et vide les champs quand le code est invalide", async () => {
|
||||||
|
registerApi.mockResolvedValue({ message: 'ok' })
|
||||||
|
verifyCodeApi.mockRejectedValue(new Error('Code invalide.'))
|
||||||
|
|
||||||
|
renderWithProviders(<Inscription />, { route: '/inscription' })
|
||||||
|
await remplirEtSoumettre()
|
||||||
|
await screen.findByRole('heading', { name: /vérifiez votre email/i })
|
||||||
|
|
||||||
|
await saisirCode('0000')
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /valider le code/i }))
|
||||||
|
|
||||||
|
expect(await screen.findByText('Code invalide.')).toBeInTheDocument()
|
||||||
|
for (const input of screen.getAllByRole('textbox')) {
|
||||||
|
expect(input).toHaveValue('')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
80
frontend/src/test/fonctionnel/ProtectedRoute.test.jsx
Normal file
80
frontend/src/test/fonctionnel/ProtectedRoute.test.jsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { screen } from '@testing-library/react'
|
||||||
|
import ProtectedRoute from '../../components/ProtectedRoute'
|
||||||
|
import { renderWithProviders } from '../renderWithProviders'
|
||||||
|
|
||||||
|
describe('ProtectedRoute', () => {
|
||||||
|
it("redirige vers /connexion si l'utilisateur n'est pas connecté", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<ProtectedRoute>
|
||||||
|
<p>Contenu protégé</p>
|
||||||
|
</ProtectedRoute>,
|
||||||
|
{
|
||||||
|
route: '/mes-gains',
|
||||||
|
routes: { '/connexion': <p>Page Connexion</p> },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Page Connexion')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Contenu protégé')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("affiche le contenu si l'utilisateur est connecté", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<ProtectedRoute>
|
||||||
|
<p>Contenu protégé</p>
|
||||||
|
</ProtectedRoute>,
|
||||||
|
{
|
||||||
|
route: '/mes-gains',
|
||||||
|
user: { id: 1, email: 'client@example.com', role: 'CLIENT' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Contenu protégé')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche le contenu si le rôle est autorisé', () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<ProtectedRoute roles={['ADMIN']}>
|
||||||
|
<p>Espace admin</p>
|
||||||
|
</ProtectedRoute>,
|
||||||
|
{
|
||||||
|
route: '/admin',
|
||||||
|
user: { id: 2, email: 'admin@example.com', role: 'ADMIN' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Espace admin')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("redirige un client vers son espace s'il tente d'accéder à l'admin", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<ProtectedRoute roles={['ADMIN']}>
|
||||||
|
<p>Espace admin</p>
|
||||||
|
</ProtectedRoute>,
|
||||||
|
{
|
||||||
|
route: '/admin',
|
||||||
|
user: { id: 1, email: 'client@example.com', role: 'CLIENT' },
|
||||||
|
routes: { '/mes-gains': <p>Page Mes Gains</p> },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Page Mes Gains')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Espace admin')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("redirige un employé vers son espace s'il tente d'accéder à l'admin", () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<ProtectedRoute roles={['ADMIN']}>
|
||||||
|
<p>Espace admin</p>
|
||||||
|
</ProtectedRoute>,
|
||||||
|
{
|
||||||
|
route: '/admin',
|
||||||
|
user: { id: 3, email: 'employe@example.com', role: 'EMPLOYEE' },
|
||||||
|
routes: { '/employe': <p>Page Employé</p> },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Page Employé')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
93
frontend/src/test/fonctionnel/SaisirCodeUnique.test.jsx
Normal file
93
frontend/src/test/fonctionnel/SaisirCodeUnique.test.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import SaisirCodeUnique from '../../pages/SaisirCodeUnique'
|
||||||
|
import Perdu from '../../pages/Perdu'
|
||||||
|
import { renderWithProviders } from '../renderWithProviders'
|
||||||
|
import { participateApi } from '../../services/game'
|
||||||
|
|
||||||
|
vi.mock('../../services/game', () => ({
|
||||||
|
participateApi: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const client = { id: 1, email: 'client@example.com', role: 'CLIENT' }
|
||||||
|
|
||||||
|
async function saisirEtValider(code) {
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre code/i), code)
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /valider/i }))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SaisirCodeUnique', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('désactive le bouton tant que le code ne fait pas 10 caractères', async () => {
|
||||||
|
renderWithProviders(<SaisirCodeUnique />, { route: '/saisir-code-unique', user: client })
|
||||||
|
|
||||||
|
const bouton = screen.getByRole('button', { name: /valider/i })
|
||||||
|
expect(bouton).toBeDisabled()
|
||||||
|
|
||||||
|
await userEvent.type(screen.getByPlaceholderText(/votre code/i), 'ABCDE12345')
|
||||||
|
expect(bouton).toBeEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('met le code en majuscules et l\'envoie à l\'API', async () => {
|
||||||
|
participateApi.mockResolvedValue({ gain: { name: 'Thé détox' }, claim: { id: 1 } })
|
||||||
|
|
||||||
|
renderWithProviders(<SaisirCodeUnique />, { route: '/saisir-code-unique', user: client })
|
||||||
|
await saisirEtValider('abcde12345')
|
||||||
|
|
||||||
|
expect(participateApi).toHaveBeenCalledWith('ABCDE12345')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("affiche l'écran Félicitation puis le détail du lot quand le code est gagnant", async () => {
|
||||||
|
participateApi.mockResolvedValue({
|
||||||
|
gain: { name: 'Thé signature', description: 'Un thé d\'exception', value: 39 },
|
||||||
|
claim: { id: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
renderWithProviders(<SaisirCodeUnique />, { route: '/saisir-code-unique', user: client })
|
||||||
|
await saisirEtValider('ABCDE12345')
|
||||||
|
|
||||||
|
// Overlay de félicitation
|
||||||
|
expect(await screen.findByText(/félicitation/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Thé signature')).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Détail du lot
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /découvrir mon lot/i }))
|
||||||
|
expect(screen.getByText(/valeur estimée/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('39 €')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/en attente de remise/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("redirige vers /perdu avec le message de l'API quand le code est invalide", async () => {
|
||||||
|
participateApi.mockRejectedValue(new Error('Ce code a déjà été utilisé.'))
|
||||||
|
|
||||||
|
renderWithProviders(<SaisirCodeUnique />, {
|
||||||
|
route: '/saisir-code-unique',
|
||||||
|
user: client,
|
||||||
|
routes: { '/perdu': <Perdu /> },
|
||||||
|
})
|
||||||
|
await saisirEtValider('ABCDE12345')
|
||||||
|
|
||||||
|
expect(await screen.findByText(/réessayer la prochaine fois/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Ce code a déjà été utilisé.')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('déconnecte et redirige vers /connexion si la session a expiré (401)', async () => {
|
||||||
|
participateApi.mockRejectedValue(
|
||||||
|
Object.assign(new Error('Session expirée'), { status: 401 })
|
||||||
|
)
|
||||||
|
|
||||||
|
renderWithProviders(<SaisirCodeUnique />, {
|
||||||
|
route: '/saisir-code-unique',
|
||||||
|
user: client,
|
||||||
|
routes: { '/connexion': <p>Page Connexion</p> },
|
||||||
|
})
|
||||||
|
await saisirEtValider('ABCDE12345')
|
||||||
|
|
||||||
|
expect(await screen.findByText('Page Connexion')).toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('token')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
29
frontend/src/test/renderWithProviders.jsx
Normal file
29
frontend/src/test/renderWithProviders.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
||||||
|
import { AuthProvider } from '../context/AuthContext'
|
||||||
|
|
||||||
|
// Rend un composant avec les mêmes providers que l'appli (AuthContext + Router).
|
||||||
|
// - `route` : URL initiale (peut être un objet { pathname, state })
|
||||||
|
// - `user` : utilisateur pré-connecté (écrit dans localStorage avant le rendu,
|
||||||
|
// comme le ferait une vraie session)
|
||||||
|
// - `routes`: routes supplémentaires pour vérifier les redirections,
|
||||||
|
// ex: { '/mes-gains': <p>Page Mes Gains</p> }
|
||||||
|
export function renderWithProviders(ui, { route = '/', user = null, routes = {} } = {}) {
|
||||||
|
if (user) {
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
localStorage.setItem('token', 'fake-token')
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<AuthProvider>
|
||||||
|
<MemoryRouter initialEntries={[route]}>
|
||||||
|
<Routes>
|
||||||
|
<Route path={typeof route === 'string' ? route : route.pathname} element={ui} />
|
||||||
|
{Object.entries(routes).map(([path, element]) => (
|
||||||
|
<Route key={path} path={path} element={element} />
|
||||||
|
))}
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
71
frontend/src/test/unit/AuthContext.test.jsx
Normal file
71
frontend/src/test/unit/AuthContext.test.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { AuthProvider, useAuth } from '../../context/AuthContext'
|
||||||
|
|
||||||
|
// Petit composant de test pour manipuler le contexte comme le ferait une page.
|
||||||
|
function Consumer() {
|
||||||
|
const { user, login, logout } = useAuth()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{user ? `Connecté : ${user.email}` : 'Déconnecté'}</p>
|
||||||
|
<button onClick={() => login({ email: 'a@b.fr', role: 'CLIENT' }, 'mon-jwt')}>
|
||||||
|
se connecter
|
||||||
|
</button>
|
||||||
|
<button onClick={logout}>se déconnecter</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AuthContext', () => {
|
||||||
|
it("n'a pas d'utilisateur par défaut", () => {
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<Consumer />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
expect(screen.getByText('Déconnecté')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('restaure la session depuis le localStorage', () => {
|
||||||
|
localStorage.setItem('user', JSON.stringify({ email: 'deja@la.fr', role: 'CLIENT' }))
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<Consumer />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
expect(screen.getByText('Connecté : deja@la.fr')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('login met à jour le user et écrit token + user dans le localStorage', async () => {
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<Consumer />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'se connecter' }))
|
||||||
|
|
||||||
|
expect(screen.getByText('Connecté : a@b.fr')).toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('token')).toBe('mon-jwt')
|
||||||
|
expect(JSON.parse(localStorage.getItem('user'))).toEqual({ email: 'a@b.fr', role: 'CLIENT' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logout vide le user et le localStorage', async () => {
|
||||||
|
localStorage.setItem('token', 'mon-jwt')
|
||||||
|
localStorage.setItem('user', JSON.stringify({ email: 'a@b.fr', role: 'CLIENT' }))
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AuthProvider>
|
||||||
|
<Consumer />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'se déconnecter' }))
|
||||||
|
|
||||||
|
expect(screen.getByText('Déconnecté')).toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('token')).toBeNull()
|
||||||
|
expect(localStorage.getItem('user')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
102
frontend/src/test/unit/auth.service.test.js
Normal file
102
frontend/src/test/unit/auth.service.test.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { loginApi, registerApi, verifyCodeApi, changePasswordApi } from '../../services/auth'
|
||||||
|
|
||||||
|
// Tests unitaires du service auth : on mocke fetch, aucune vraie requête HTTP.
|
||||||
|
const mockFetch = vi.fn()
|
||||||
|
|
||||||
|
function jsonResponse(body, { ok = true, status = 200 } = {}) {
|
||||||
|
return Promise.resolve({ ok, status, json: () => Promise.resolve(body) })
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal('fetch', mockFetch)
|
||||||
|
mockFetch.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('loginApi', () => {
|
||||||
|
it('envoie email et mot de passe et renvoie les données', async () => {
|
||||||
|
const payload = { token: 'jwt', user: { email: 'a@b.fr', role: 'CLIENT' } }
|
||||||
|
mockFetch.mockReturnValue(jsonResponse(payload))
|
||||||
|
|
||||||
|
const data = await loginApi({ email: 'a@b.fr', password: 'secret' })
|
||||||
|
|
||||||
|
expect(data).toEqual(payload)
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: 'a@b.fr', password: 'secret' }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("lève une erreur avec le message et le status de l'API en cas d'échec", async () => {
|
||||||
|
mockFetch.mockReturnValue(
|
||||||
|
jsonResponse({ message: 'Identifiants invalides.' }, { ok: false, status: 401 })
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(loginApi({ email: 'a@b.fr', password: 'faux' })).rejects.toMatchObject({
|
||||||
|
message: 'Identifiants invalides.',
|
||||||
|
status: 401,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("utilise un message par défaut si l'API n'en renvoie pas", async () => {
|
||||||
|
mockFetch.mockReturnValue(jsonResponse({}, { ok: false, status: 500 }))
|
||||||
|
|
||||||
|
await expect(loginApi({ email: 'a@b.fr', password: 'x' })).rejects.toThrow(
|
||||||
|
'Erreur de connexion.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('registerApi', () => {
|
||||||
|
it("envoie les informations d'inscription", async () => {
|
||||||
|
mockFetch.mockReturnValue(jsonResponse({ message: 'ok' }))
|
||||||
|
|
||||||
|
await registerApi({ email: 'a@b.fr', firstname: 'Jean', lastname: 'Dupont', password: 'secret' })
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: 'a@b.fr', firstname: 'Jean', lastname: 'Dupont', password: 'secret' }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('verifyCodeApi', () => {
|
||||||
|
it('renvoie le token et le user quand le code est valide', async () => {
|
||||||
|
const payload = { token: 'jwt', user: { email: 'a@b.fr' } }
|
||||||
|
mockFetch.mockReturnValue(jsonResponse(payload))
|
||||||
|
|
||||||
|
const data = await verifyCodeApi({ email: 'a@b.fr', code: '1234' })
|
||||||
|
|
||||||
|
expect(data).toEqual(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lève "Code invalide." par défaut en cas d\'échec', async () => {
|
||||||
|
mockFetch.mockReturnValue(jsonResponse({}, { ok: false, status: 400 }))
|
||||||
|
|
||||||
|
await expect(verifyCodeApi({ email: 'a@b.fr', code: '0000' })).rejects.toThrow('Code invalide.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('changePasswordApi', () => {
|
||||||
|
it('envoie le token du localStorage dans le header Authorization', async () => {
|
||||||
|
localStorage.setItem('token', 'mon-jwt')
|
||||||
|
mockFetch.mockReturnValue(jsonResponse({ message: 'ok' }))
|
||||||
|
|
||||||
|
await changePasswordApi({ currentPassword: 'ancien', newPassword: 'nouveau' })
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('/api/auth/change-password', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer mon-jwt',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ currentPassword: 'ancien', newPassword: 'nouveau' }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
68
frontend/src/test/unit/game.service.test.js
Normal file
68
frontend/src/test/unit/game.service.test.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { myGainsApi, participateApi } from '../../services/game'
|
||||||
|
|
||||||
|
// Tests unitaires du service game : on mocke fetch, aucune vraie requête HTTP.
|
||||||
|
const mockFetch = vi.fn()
|
||||||
|
|
||||||
|
function jsonResponse(body, { ok = true, status = 200 } = {}) {
|
||||||
|
return Promise.resolve({ ok, status, json: () => Promise.resolve(body) })
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal('fetch', mockFetch)
|
||||||
|
mockFetch.mockReset()
|
||||||
|
localStorage.setItem('token', 'mon-jwt')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('myGainsApi', () => {
|
||||||
|
it('appelle /api/game/my-gains avec le token et renvoie les gains', async () => {
|
||||||
|
const payload = { gains: [{ id: 1, name: 'Thé détox' }] }
|
||||||
|
mockFetch.mockReturnValue(jsonResponse(payload))
|
||||||
|
|
||||||
|
const data = await myGainsApi()
|
||||||
|
|
||||||
|
expect(data).toEqual(payload)
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('/api/game/my-gains', {
|
||||||
|
headers: { Authorization: 'Bearer mon-jwt' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("lève une erreur avec le status en cas d'échec (ex: session expirée)", async () => {
|
||||||
|
mockFetch.mockReturnValue(
|
||||||
|
jsonResponse({ message: 'Non autorisé.' }, { ok: false, status: 401 })
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(myGainsApi()).rejects.toMatchObject({ message: 'Non autorisé.', status: 401 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('participateApi', () => {
|
||||||
|
it('envoie le code en POST avec le token', async () => {
|
||||||
|
const payload = { gain: { name: 'Thé signature' }, claim: { id: 1 } }
|
||||||
|
mockFetch.mockReturnValue(jsonResponse(payload))
|
||||||
|
|
||||||
|
const data = await participateApi('ABCDE12345')
|
||||||
|
|
||||||
|
expect(data).toEqual(payload)
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('/api/game/participate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer mon-jwt',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ code: 'ABCDE12345' }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("transmet le message d'erreur de l'API (code déjà utilisé, invalide...)", async () => {
|
||||||
|
mockFetch.mockReturnValue(
|
||||||
|
jsonResponse({ message: 'Ce code a déjà été utilisé.' }, { ok: false, status: 400 })
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(participateApi('ABCDE12345')).rejects.toThrow('Ce code a déjà été utilisé.')
|
||||||
|
})
|
||||||
|
})
|
||||||
51
frontend/src/test/unit/useHandleAuthError.test.jsx
Normal file
51
frontend/src/test/unit/useHandleAuthError.test.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { useHandleAuthError } from '../../hooks/useHandleAuthError'
|
||||||
|
import { renderWithProviders } from '../renderWithProviders'
|
||||||
|
|
||||||
|
// Composant de test : déclenche le hook avec l'erreur passée en prop.
|
||||||
|
function TestComponent({ error }) {
|
||||||
|
const handleAuthError = useHandleAuthError()
|
||||||
|
const [handled, setHandled] = useState(null)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setHandled(handleAuthError(error))}>déclencher</button>
|
||||||
|
{handled !== null && <p>géré : {String(handled)}</p>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useHandleAuthError', () => {
|
||||||
|
it('déconnecte et redirige vers /connexion sur une erreur 401', async () => {
|
||||||
|
const error = Object.assign(new Error('Session expirée'), { status: 401 })
|
||||||
|
|
||||||
|
renderWithProviders(<TestComponent error={error} />, {
|
||||||
|
route: '/mes-gains',
|
||||||
|
user: { email: 'a@b.fr', role: 'CLIENT' },
|
||||||
|
routes: { '/connexion': <p>Page Connexion</p> },
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'déclencher' }))
|
||||||
|
|
||||||
|
expect(screen.getByText('Page Connexion')).toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('token')).toBeNull()
|
||||||
|
expect(localStorage.getItem('user')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ne fait rien et renvoie false pour une autre erreur', async () => {
|
||||||
|
const error = Object.assign(new Error('Erreur serveur'), { status: 500 })
|
||||||
|
|
||||||
|
renderWithProviders(<TestComponent error={error} />, {
|
||||||
|
route: '/mes-gains',
|
||||||
|
user: { email: 'a@b.fr', role: 'CLIENT' },
|
||||||
|
routes: { '/connexion': <p>Page Connexion</p> },
|
||||||
|
})
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'déclencher' }))
|
||||||
|
|
||||||
|
expect(screen.getByText('géré : false')).toBeInTheDocument()
|
||||||
|
expect(localStorage.getItem('token')).toBe('fake-token')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,4 +9,9 @@ export default defineConfig({
|
|||||||
'/api': 'http://localhost:5000',
|
'/api': 'http://localhost:5000',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
setupFiles: './src/setupTests.js',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user