Files
Classeo/frontend/e2e/token-edge-cases.spec.ts
Mathias STRASSER dc2be898d5
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Provisionner automatiquement un nouvel établissement
Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
2026-04-16 09:27:25 +02:00

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: 15000 });
// 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 });
});
});
});