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 navigation links (scoped to desktop nav to avoid action cards) const nav = page.locator('.desktop-nav'); await expect(nav.getByRole('link', { name: 'Utilisateurs' })).toBeVisible({ timeout: 15000 }); await expect(nav.getByRole('link', { name: 'Classes' })).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('.header-nav').getByRole('link', { name: 'Utilisateurs' }); await expect(adminUsersLink).not.toBeVisible(); }); }); });