feat: Pagination et recherche des sections admin
Les listes admin (utilisateurs, classes, matières, affectations) chargeaient toutes les données d'un coup, ce qui dégradait l'expérience avec un volume croissant. La pagination côté serveur existait dans la config API Platform mais aucun Provider ne l'exploitait. Cette implémentation ajoute la pagination serveur (30 items/page, max 100) avec recherche textuelle sur toutes les sections, des composants frontend réutilisables (Pagination + SearchInput avec debounce), et la synchronisation URL pour le partage de liens filtrés. Les Query valident leurs paramètres (clamp page/limit, trim search) pour éviter les abus. Les affectations utilisent des lookup maps pour résoudre les noms sans N+1 queries. Les pages admin gèrent les race conditions via AbortController.
This commit is contained in:
212
frontend/e2e/role-access-control.spec.ts
Normal file
212
frontend/e2e/role-access-control.spec.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
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`);
|
||||
|
||||
await expect(page).toHaveURL(/\/admin/);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: /administration/i })
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Teacher access - should NOT have access to /admin pages
|
||||
// ============================================================================
|
||||
test.describe('Teacher Access Restrictions', () => {
|
||||
test('[P0] teacher cannot access /admin/users page', async ({ page }) => {
|
||||
await loginAs(page, TEACHER_EMAIL, TEACHER_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] teacher cannot access /admin page', async ({ page }) => {
|
||||
await loginAs(page, TEACHER_EMAIL, TEACHER_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');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 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 header nav to avoid action cards)
|
||||
const nav = page.locator('.header-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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user