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' : ''; 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 reset token from output:', result); return null; } catch (error) { console.error('Failed to create reset token:', error); return null; } } /** * Helper to create an activation token via CLI command. */ function createActivationToken(options: { email: string; tenant?: string }): string | null { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); const tenant = options.tenant ?? 'ecole-alpha'; try { const result = execSync( `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-activation-token --email=${options.email} --tenant=${tenant} 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 activation token from output:', result); return null; } catch (error) { console.error('Failed to create activation token:', error); return null; } } test.describe('Expired/Invalid Token Scenarios [P0]', () => { // ============================================================================ // Activation Token Edge Cases // ============================================================================ test.describe('Activation Token Validation', () => { test('[P0] invalid activation token format shows error', async ({ page }) => { // Use a clearly invalid token format (not a UUID) await page.goto('/activate/invalid-token-not-a-uuid'); // Should show an error indicating the link is invalid await expect( page.getByRole('heading', { name: /lien invalide/i }) ).toBeVisible({ timeout: 10000 }); await expect( page.getByText(/contacter votre établissement/i) ).toBeVisible(); }); test('[P0] non-existent activation token shows error', async ({ page }) => { // Use a valid UUID format but a token that does not exist await page.goto('/activate/00000000-0000-0000-0000-000000000000'); // Should show error because token doesn't exist in the database await expect( page.getByRole('heading', { name: /lien invalide/i }) ).toBeVisible({ timeout: 10000 }); }); test('[P0] reusing an already-used activation token shows error', async ({ page }, testInfo) => { const email = `e2e-reuse-act-${testInfo.project.name}-${Date.now()}@example.com`; const token = createActivationToken({ email }); test.skip(!token, 'Could not create test activation token'); // First activation: use the token await page.goto(`/activate/${token}`); const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); // Fill valid password (must include special char for 5/5 requirements) await page.locator('#password').fill('SecurePass123!'); await page.locator('#passwordConfirmation').fill('SecurePass123!'); const submitButton = page.getByRole('button', { name: /activer mon compte/i }); await expect(submitButton).toBeEnabled({ timeout: 2000 }); // Submit the activation await submitButton.click(); // Wait for successful activation (redirects to login with query param) await expect(page).toHaveURL(/\/login\?activated=true/, { timeout: 10000 }); // Second attempt: try to reuse the same token await page.goto(`/activate/${token}`); // Should show an error because the token has already been consumed await expect( page.getByRole('heading', { name: /lien invalide/i }) ).toBeVisible({ timeout: 10000 }); }); }); // ============================================================================ // Password Reset Token Edge Cases // ============================================================================ test.describe('Password Reset Token Validation', () => { test('[P0] invalid password reset token shows error after submission', async ({ page }) => { // Use a valid UUID format but a token that does not exist await page.goto('/reset-password/00000000-0000-0000-0000-000000000000'); // The form is shown initially (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('[P0] expired password reset token shows error after submission', async ({ page }, testInfo) => { const email = `e2e-exp-reset-${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/invalid error await expect( page.getByRole('heading', { name: 'Lien invalide' }) ).toBeVisible({ timeout: 10000 }); }); test('[P0] reusing a password reset token shows error', async ({ page }, testInfo) => { const email = `e2e-reuse-reset-${testInfo.project.name}-${Date.now()}@example.com`; const token = createResetToken({ email }); test.skip(!token, 'Could not create test token'); // First reset: use the token successfully 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 await expect( page.getByRole('heading', { name: 'Mot de passe modifié' }) ).toBeVisible({ timeout: 10000 }); // Second attempt: try to reuse the same token 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 (token already used) await expect( page.getByRole('heading', { name: 'Lien invalide' }) ).toBeVisible({ timeout: 10000 }); }); }); });