Files
Classeo/frontend/e2e/password-reset.spec.ts
Mathias STRASSER b823479658 feat: Gestion des sessions utilisateur
Permet aux utilisateurs de visualiser et gérer leurs sessions actives
sur différents appareils, avec la possibilité de révoquer des sessions
à distance en cas de suspicion d'activité non autorisée.

Fonctionnalités :
- Liste des sessions actives avec métadonnées (appareil, navigateur, localisation)
- Identification de la session courante
- Révocation individuelle d'une session
- Révocation de toutes les autres sessions
- Déconnexion avec nettoyage des cookies sur les deux chemins (legacy et actuel)

Sécurité :
- Cache frontend scopé par utilisateur pour éviter les fuites entre comptes
- Validation que le refresh token appartient à l'utilisateur JWT authentifié
- TTL des sessions Redis aligné sur l'expiration du refresh token
- Événements d'audit pour traçabilité (SessionInvalidee, ToutesSessionsInvalidees)

@see Story 1.6 - Gestion des sessions
2026-02-03 10:53:31 +01:00

272 lines
10 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { execSync } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Helper to create a password reset token via CLI command
*/
function createResetToken(options: { email: string; expired?: boolean }): string | null {
const projectRoot = join(__dirname, '../..');
const composeFile = join(projectRoot, 'compose.yaml');
try {
const expiredFlag = options.expired ? ' --expired' : '';
// Use dev environment to match the running web server
const result = execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-password-reset-token --email=${options.email}${expiredFlag} 2>&1`,
{ encoding: 'utf-8' }
);
const tokenMatch = result.match(/Token\s+([a-f0-9-]{36})/i);
if (tokenMatch) {
return tokenMatch[1];
}
console.error('Could not extract token from output:', result);
return null;
} catch (error) {
console.error('Failed to create reset token:', error);
return null;
}
}
test.describe('Password Reset Flow', () => {
test.describe('Forgot Password Page (/mot-de-passe-oublie)', () => {
test('displays the forgot password form', async ({ page }) => {
await page.goto('/mot-de-passe-oublie');
// Page should have the heading and form elements
await expect(page.getByRole('heading', { name: /mot de passe oublié/i })).toBeVisible();
await expect(page.locator('input#email')).toBeVisible();
await expect(
page.getByRole('button', { name: /envoyer le lien de réinitialisation/i })
).toBeVisible();
});
test('shows success message after submitting email', async ({ page }) => {
await page.goto('/mot-de-passe-oublie');
const emailInput = page.locator('input#email');
await emailInput.fill('test-forgot@example.com');
await page.getByRole('button', { name: /envoyer le lien de réinitialisation/i }).click();
// Should show success message (always, to prevent enumeration)
await expect(page.getByText(/vérifiez votre boîte mail/i)).toBeVisible({ timeout: 10000 });
});
test('has link to login page', async ({ page }) => {
await page.goto('/mot-de-passe-oublie');
const loginLink = page.getByRole('link', { name: /retour à la connexion/i });
await expect(loginLink).toBeVisible();
});
});
test.describe('Reset Password Page (/reset-password/[token])', () => {
test.describe('Password Form Display', () => {
test('displays password form', async ({ page }, testInfo) => {
// Create a fresh token for this test
const email = `e2e-form-display-${testInfo.project.name}-${Date.now()}@example.com`;
const token = createResetToken({ email });
test.skip(!token, 'Could not create test token');
await page.goto(`/reset-password/${token}`);
// Form should be visible
await expect(page.locator('form')).toBeVisible({ timeout: 5000 });
await expect(page.locator('#password')).toBeVisible();
await expect(page.locator('#confirmPassword')).toBeVisible();
});
test('validates password requirements in real-time', async ({ page }, testInfo) => {
const email = `e2e-validation-${testInfo.project.name}-${Date.now()}@example.com`;
const token = createResetToken({ email });
test.skip(!token, 'Could not create test token');
await page.goto(`/reset-password/${token}`);
const form = page.locator('form');
await expect(form).toBeVisible({ timeout: 5000 });
const passwordInput = page.locator('#password');
// Test short password - should not pass length requirement
await passwordInput.fill('Abc1!');
const minLengthItem = page
.locator('.requirements li')
.filter({ hasText: /8 caractères/ });
await expect(minLengthItem).not.toHaveClass(/valid/);
// Test missing uppercase
await passwordInput.fill('abcdefgh1!');
const uppercaseItem = page
.locator('.requirements li')
.filter({ hasText: /majuscule/ });
await expect(uppercaseItem).not.toHaveClass(/valid/);
// Valid password should show all checkmarks (5 requirements)
await passwordInput.fill('Abcdefgh1!');
const validItems = page.locator('.requirements li.valid');
await expect(validItems).toHaveCount(5);
});
test('validates password confirmation matches', async ({ page }, testInfo) => {
const email = `e2e-confirm-${testInfo.project.name}-${Date.now()}@example.com`;
const token = createResetToken({ email });
test.skip(!token, 'Could not create test token');
await page.goto(`/reset-password/${token}`);
const form = page.locator('form');
await expect(form).toBeVisible({ timeout: 5000 });
const passwordInput = page.locator('#password');
const confirmInput = page.locator('#confirmPassword');
await passwordInput.fill('SecurePass123!');
await confirmInput.fill('DifferentPass123!');
// Should show mismatch error
await expect(page.getByText(/mots de passe ne correspondent pas/i)).toBeVisible();
// Fix confirmation
await confirmInput.fill('SecurePass123!');
await expect(page.getByText(/mots de passe ne correspondent pas/i)).not.toBeVisible();
});
test('submit button is disabled until form is valid', async ({ page }, testInfo) => {
const email = `e2e-button-${testInfo.project.name}-${Date.now()}@example.com`;
const token = createResetToken({ email });
test.skip(!token, 'Could not create test token');
await page.goto(`/reset-password/${token}`);
const form = page.locator('form');
await expect(form).toBeVisible({ timeout: 5000 });
const submitButton = page.getByRole('button', { name: /réinitialiser/i });
// Initially disabled
await expect(submitButton).toBeDisabled();
// Fill valid password
await page.locator('#password').fill('NewSecurePass123!');
await page.locator('#confirmPassword').fill('NewSecurePass123!');
// Should now be enabled
await expect(submitButton).toBeEnabled();
});
});
test.describe('Token Validation', () => {
test('shows error for invalid token after form submission', async ({ page }) => {
// Use an invalid token format
await page.goto('/reset-password/00000000-0000-0000-0000-000000000000');
// Form should still be visible (validation happens on submit)
const form = page.locator('form');
await expect(form).toBeVisible({ timeout: 5000 });
// Fill valid password and submit
await page.locator('#password').fill('ValidPassword123!');
await page.locator('#confirmPassword').fill('ValidPassword123!');
await page.getByRole('button', { name: /réinitialiser/i }).click();
// Should show error after submission
await expect(page.getByRole('heading', { name: 'Lien invalide' })).toBeVisible({
timeout: 10000
});
});
test('shows expiration message for expired token after form submission', async ({
page
}, testInfo) => {
const email = `e2e-expired-${testInfo.project.name}-${Date.now()}@example.com`;
const token = createResetToken({ email, expired: true });
test.skip(!token, 'Could not create expired test token');
await page.goto(`/reset-password/${token}`);
const form = page.locator('form');
await expect(form).toBeVisible({ timeout: 5000 });
// Fill valid password and submit
await page.locator('#password').fill('ValidPassword123!');
await page.locator('#confirmPassword').fill('ValidPassword123!');
await page.getByRole('button', { name: /réinitialiser/i }).click();
// Should show expired/used error (shows "Lien invalide" heading with expiry message)
await expect(page.getByRole('heading', { name: 'Lien invalide' })).toBeVisible({
timeout: 10000
});
});
});
test.describe('Complete Reset Flow', () => {
test('successfully resets password and redirects to login', async ({ page }, testInfo) => {
const email = `e2e-success-${testInfo.project.name}-${Date.now()}@example.com`;
const token = createResetToken({ email });
test.skip(!token, 'Could not create test token');
await page.goto(`/reset-password/${token}`);
const form = page.locator('form');
await expect(form).toBeVisible({ timeout: 5000 });
// Fill form
await page.locator('#password').fill('NewSecurePass123!');
await page.locator('#confirmPassword').fill('NewSecurePass123!');
const submitButton = page.getByRole('button', { name: /réinitialiser/i });
await expect(submitButton).toBeEnabled();
// Submit
await submitButton.click();
// Should show success message (heading "Mot de passe modifié")
await expect(page.getByRole('heading', { name: 'Mot de passe modifié' })).toBeVisible({
timeout: 10000
});
});
test('token cannot be reused after successful reset', async ({ page }, testInfo) => {
const email = `e2e-reuse-${testInfo.project.name}-${Date.now()}@example.com`;
const token = createResetToken({ email });
test.skip(!token, 'Could not create test token');
// First reset
await page.goto(`/reset-password/${token}`);
await expect(page.locator('form')).toBeVisible({ timeout: 5000 });
await page.locator('#password').fill('FirstPassword123!');
await page.locator('#confirmPassword').fill('FirstPassword123!');
await page.getByRole('button', { name: /réinitialiser/i }).click();
// Wait for success (heading "Mot de passe modifié")
await expect(page.getByRole('heading', { name: 'Mot de passe modifié' })).toBeVisible({
timeout: 10000
});
// Try to use same token again
await page.goto(`/reset-password/${token}`);
await expect(page.locator('form')).toBeVisible({ timeout: 5000 });
await page.locator('#password').fill('SecondPassword123!');
await page.locator('#confirmPassword').fill('SecondPassword123!');
await page.getByRole('button', { name: /réinitialiser/i }).click();
// Should show error (already used) - page shows "Lien invalide" heading
await expect(page.getByRole('heading', { name: 'Lien invalide' })).toBeVisible({
timeout: 10000
});
});
});
});
});