Le menu d'administration contenait 13 liens à plat dans le header, ce qui débordait sur desktop et rendait le drawer mobile trop long à scanner. Les liens sont maintenant regroupés en 4 catégories (Personnes, Organisation, Année scolaire, Paramètres) avec des dropdowns au survol sur desktop et des accordéons repliables dans le drawer mobile. Le nombre d'éléments visibles passe de 13 à 5 (1 lien direct + 4 catégories), la catégorie active s'auto-déplie dans le menu mobile.
210 lines
8.4 KiB
TypeScript
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('.header-nav').getByRole('link', { name: 'Utilisateurs' });
|
|
await expect(adminUsersLink).not.toBeVisible();
|
|
});
|
|
});
|
|
});
|