Couverture des processors (RefreshToken, RequestPasswordReset, ResetPassword, SwitchRole, UpdateUserRoles), des query handlers (HasGradesInPeriod, HasStudentsInClass), des messaging handlers (SendActivationConfirmation, SendPasswordResetEmail), et côté frontend des modules auth, roles, monitoring, types et E2E tokens.
202 lines
7.5 KiB
TypeScript
202 lines
7.5 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' : '';
|
|
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 });
|
|
});
|
|
});
|
|
});
|