Files
Classeo/frontend/e2e/role-access-control.spec.ts
Mathias STRASSER e745cf326a
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: Calculer automatiquement les moyennes après chaque saisie de notes
Les enseignants ont besoin de moyennes à jour immédiatement après la
publication ou modification des notes, sans attendre un batch nocturne.

Le système recalcule via Domain Events synchrones : statistiques
d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées
(normalisation /20), et moyenne générale par élève. Les résultats sont
stockés dans des tables dénormalisées avec cache Redis (TTL 5 min).

Trois endpoints API exposent les données avec contrôle d'accès par rôle.
Une commande console permet le backfill des données historiques au
déploiement.
2026-04-02 06:45:41 +02:00

210 lines
8.4 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);
// Extract port from PLAYWRIGHT_BASE_URL or use default (4173 matches playwright.config.ts)
const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:4173';
const urlMatch = baseUrl.match(/:(\d+)$/);
const PORT = urlMatch ? urlMatch[1] : '4173';
const ALPHA_URL = `http://ecole-alpha.classeo.local:${PORT}`;
// Test credentials per role
const ADMIN_EMAIL = 'e2e-rbac-admin@example.com';
const ADMIN_PASSWORD = 'RbacAdmin123';
const TEACHER_EMAIL = 'e2e-rbac-teacher@example.com';
const TEACHER_PASSWORD = 'RbacTeacher123';
const PARENT_EMAIL = 'e2e-rbac-parent@example.com';
const PARENT_PASSWORD = 'RbacParent123';
test.describe('Role-Based Access Control [P0]', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async () => {
const projectRoot = join(__dirname, '../..');
const composeFile = join(projectRoot, 'compose.yaml');
// Create admin user
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${ADMIN_EMAIL} --password=${ADMIN_PASSWORD} --role=ROLE_ADMIN 2>&1`,
{ encoding: 'utf-8' }
);
// Create teacher user
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${TEACHER_EMAIL} --password=${TEACHER_PASSWORD} --role=ROLE_PROF 2>&1`,
{ encoding: 'utf-8' }
);
// Create parent user
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${PARENT_EMAIL} --password=${PARENT_PASSWORD} --role=ROLE_PARENT 2>&1`,
{ encoding: 'utf-8' }
);
});
async function loginAs(
page: import('@playwright/test').Page,
email: string,
password: string
) {
await page.goto(`${ALPHA_URL}/login`);
await page.locator('#email').fill(email);
await page.locator('#password').fill(password);
await Promise.all([
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
page.getByRole('button', { name: /se connecter/i }).click()
]);
}
// ============================================================================
// Admin access - should have access to all /admin pages
// ============================================================================
test.describe('Admin Access', () => {
test('[P0] admin user can access /admin/users page', async ({ page }) => {
await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto(`${ALPHA_URL}/admin/users`);
// Admin should see the users management page
await expect(page).toHaveURL(/\/admin\/users/);
await expect(
page.locator('.users-table, .empty-state')
).toBeVisible({ timeout: 10000 });
});
test('[P0] admin user can access /admin/classes page', async ({ page }) => {
await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto(`${ALPHA_URL}/admin/classes`);
await expect(page).toHaveURL(/\/admin\/classes/);
await expect(
page.getByRole('heading', { name: /gestion des classes/i })
).toBeVisible({ timeout: 10000 });
});
test('[P0] admin user can access /admin/pedagogy page', async ({ page }) => {
await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto(`${ALPHA_URL}/admin/pedagogy`);
await expect(page).toHaveURL(/\/admin\/pedagogy/);
});
test('[P0] admin user can access /admin page', async ({ page }) => {
await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto(`${ALPHA_URL}/admin`);
// /admin redirects to /admin/users
await expect(page).toHaveURL(/\/admin\/users/);
await expect(
page.getByRole('heading', { name: /gestion des utilisateurs/i })
).toBeVisible({ timeout: 10000 });
});
});
// ============================================================================
// Teacher access - CAN access /admin layout (for image-rights, pedagogy)
// but backend protects sensitive pages (users, classes, etc.) with 403
// ============================================================================
test.describe('Teacher Access Restrictions', () => {
test('[P0] teacher can access /admin layout', async ({ page }) => {
await loginAs(page, TEACHER_EMAIL, TEACHER_PASSWORD);
await page.goto(`${ALPHA_URL}/admin/image-rights`);
// Teacher can access admin layout for authorized pages
await page.waitForURL(/\/admin\/image-rights/, { timeout: 30000 });
expect(page.url()).toContain('/admin/image-rights');
});
});
// ============================================================================
// Parent access - should NOT have access to /admin pages
// ============================================================================
test.describe('Parent Access Restrictions', () => {
test('[P0] parent cannot access /admin/users page', async ({ page }) => {
await loginAs(page, PARENT_EMAIL, PARENT_PASSWORD);
await page.goto(`${ALPHA_URL}/admin/users`);
// Admin guard redirects non-admin users to /dashboard
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
expect(page.url()).toContain('/dashboard');
});
test('[P0] parent cannot access /admin/classes page', async ({ page }) => {
await loginAs(page, PARENT_EMAIL, PARENT_PASSWORD);
await page.goto(`${ALPHA_URL}/admin/classes`);
// Admin guard redirects non-admin users to /dashboard
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
expect(page.url()).toContain('/dashboard');
});
test('[P0] parent cannot access /admin page', async ({ page }) => {
await loginAs(page, PARENT_EMAIL, PARENT_PASSWORD);
await page.goto(`${ALPHA_URL}/admin`);
// Admin guard redirects non-admin users to /dashboard
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
expect(page.url()).toContain('/dashboard');
});
});
// ============================================================================
// Unauthenticated user - should be redirected to /login
// ============================================================================
test.describe('Unauthenticated Access', () => {
test('[P0] unauthenticated user is redirected from /settings/sessions to /login', async ({ page }) => {
// Clear any existing session
await page.context().clearCookies();
await page.goto(`${ALPHA_URL}/settings/sessions`);
// Should be redirected to login
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
});
test('[P0] unauthenticated user is redirected from /admin/users to /login', async ({ page }) => {
await page.context().clearCookies();
await page.goto(`${ALPHA_URL}/admin/users`);
// Should be redirected away from /admin/users (to /login or /dashboard)
await page.waitForURL((url) => !url.toString().includes('/admin/users'), { timeout: 10000 });
expect(page.url()).not.toContain('/admin/users');
});
});
// ============================================================================
// Navigation reflects role permissions
// ============================================================================
test.describe('Navigation Reflects Permissions', () => {
test('[P0] admin layout shows admin navigation links', async ({ page }) => {
await loginAs(page, ADMIN_EMAIL, ADMIN_PASSWORD);
await page.goto(`${ALPHA_URL}/admin`);
// Admin layout should show grouped navigation (category triggers in desktop nav)
const nav = page.locator('.desktop-nav');
await expect(nav.getByRole('button', { name: /personnes/i })).toBeVisible({ timeout: 15000 });
await expect(nav.getByRole('button', { name: /organisation/i })).toBeVisible();
// Hover to reveal dropdown links
await nav.getByRole('button', { name: /personnes/i }).hover();
await expect(nav.getByRole('menuitem', { name: 'Utilisateurs' })).toBeVisible();
});
test('[P0] teacher sees dashboard without admin navigation', async ({ page }) => {
await loginAs(page, TEACHER_EMAIL, TEACHER_PASSWORD);
// Teacher should be on dashboard
await expect(page).toHaveURL(/\/dashboard/);
// Teacher should not see admin-specific navigation in the dashboard layout
// The dashboard header should not have admin links like "Utilisateurs"
const adminUsersLink = page.locator('.desktop-nav').getByRole('link', { name: 'Utilisateurs' });
await expect(adminUsersLink).not.toBeVisible();
});
});
});