Files
Classeo/frontend/e2e/role-access-control.spec.ts
Mathias STRASSER 36ceefb625 feat: Permettre aux élèves de consulter leur emploi du temps
Les élèves n'avaient aucun moyen de voir leur emploi du temps
depuis l'application. Cette fonctionnalité ajoute une page dédiée
avec deux modes de visualisation (jour et semaine), la navigation
temporelle, et le détail des cours au tap.

Le backend résout l'EDT de l'élève en chaînant : affectation classe →
créneaux récurrents + exceptions + calendrier scolaire → enrichissement
des noms (matières/enseignants). Le frontend utilise un cache offline
(Workbox NetworkFirst) pour rester consultable hors connexion.
2026-03-07 21:14:04 +01: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: 30000 }),
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: 30000 });
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: 30000 });
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: 30000 });
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();
});
});
});