import { test, expect } from '@playwright/test'; import { execWithRetry, runSql, clearCache, resolveDeterministicIds, createTestUser, composeFile } from './helpers'; 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}`; const ASSIGNED_TEACHER_EMAIL = 'e2e-gac-assigned@example.com'; const UNASSIGNED_TEACHER_EMAIL = 'e2e-gac-unassigned@example.com'; const REPLACEMENT_TEACHER_EMAIL = 'e2e-gac-replacement@example.com'; const PASSWORD = 'GACTest123'; const TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; let evaluationId: string; let classId: string; let subjectId: string; async function loginAs(page: import('@playwright/test').Page, email: string) { await page.goto(`${ALPHA_URL}/login`); await page.locator('#email').fill(email); await page.locator('#password').fill(PASSWORD); await page.getByRole('button', { name: /se connecter/i }).click(); await page.waitForURL(/\/dashboard/, { timeout: 60000 }); } test.describe('Grade Access Control (Story 6.9)', () => { test.describe.configure({ mode: 'serial' }); test.beforeAll(async () => { // Create users createTestUser('ecole-alpha', ASSIGNED_TEACHER_EMAIL, PASSWORD, 'ROLE_PROF'); createTestUser('ecole-alpha', UNASSIGNED_TEACHER_EMAIL, PASSWORD, 'ROLE_PROF'); createTestUser('ecole-alpha', REPLACEMENT_TEACHER_EMAIL, PASSWORD, 'ROLE_PROF'); const { schoolId, academicYearId } = resolveDeterministicIds(TENANT_ID); // Deterministic IDs classId = execWithRetry( `docker compose -f "${composeFile}" exec -T php php -r '` + `require "/app/vendor/autoload.php"; ` + `echo Ramsey\\Uuid\\Uuid::uuid5("6ba7b814-9dad-11d1-80b4-00c04fd430c8","gac-class-${TENANT_ID}")->toString();` + `' 2>&1` ).trim(); subjectId = execWithRetry( `docker compose -f "${composeFile}" exec -T php php -r '` + `require "/app/vendor/autoload.php"; ` + `echo Ramsey\\Uuid\\Uuid::uuid5("6ba7b814-9dad-11d1-80b4-00c04fd430c8","gac-subject-${TENANT_ID}")->toString();` + `' 2>&1` ).trim(); evaluationId = execWithRetry( `docker compose -f "${composeFile}" exec -T php php -r '` + `require "/app/vendor/autoload.php"; ` + `echo Ramsey\\Uuid\\Uuid::uuid5("6ba7b814-9dad-11d1-80b4-00c04fd430c8","gac-eval-${TENANT_ID}")->toString();` + `' 2>&1` ).trim(); // Create class and subject runSql( `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) ` + `VALUES ('${classId}', '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-GAC-6C', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` ); runSql( `INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) ` + `VALUES ('${subjectId}', '${TENANT_ID}', '${schoolId}', 'E2E-GAC-Maths', 'E2GACMAT', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` ); // Create one test student createTestUser('ecole-alpha', 'e2e-gac-student@example.com', PASSWORD, 'ROLE_ELEVE --firstName=Léa --lastName=Dupont'); // Assign student to class runSql( `INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) ` + `SELECT gen_random_uuid(), '${TENANT_ID}', u.id, '${classId}', '${academicYearId}', NOW(), NOW(), NOW() ` + `FROM users u WHERE u.email = 'e2e-gac-student@example.com' AND u.tenant_id = '${TENANT_ID}' ` + `ON CONFLICT (user_id, academic_year_id) DO NOTHING` ); // Assign ONLY the assigned teacher runSql( `INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at) ` + `SELECT gen_random_uuid(), '${TENANT_ID}', u.id, '${classId}', '${subjectId}', '${academicYearId}', 'active', NOW(), NOW(), NOW() ` + `FROM users u WHERE u.email = '${ASSIGNED_TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` + `ON CONFLICT DO NOTHING` ); // Create evaluation owned by assigned teacher runSql( `INSERT INTO evaluations (id, tenant_id, class_id, subject_id, teacher_id, title, evaluation_date, grade_scale, coefficient, status, created_at, updated_at) ` + `SELECT '${evaluationId}', '${TENANT_ID}', '${classId}', '${subjectId}', u.id, 'E2E-GAC Contrôle', '2026-04-15', 20, 1.0, 'published', NOW(), NOW() ` + `FROM users u WHERE u.email = '${ASSIGNED_TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` + `ON CONFLICT DO NOTHING` ); // Create active replacement for the replacement teacher (valid now → +7 days) runSql( `INSERT INTO teacher_replacements (id, tenant_id, replaced_teacher_id, replacement_teacher_id, start_date, end_date, status, reason, created_by, created_at, updated_at) ` + `SELECT gen_random_uuid(), '${TENANT_ID}', ` + `(SELECT id FROM users WHERE email = '${ASSIGNED_TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}'), ` + `(SELECT id FROM users WHERE email = '${REPLACEMENT_TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}'), ` + `NOW() - INTERVAL '1 day', NOW() + INTERVAL '7 days', 'active', 'Maladie', ` + `(SELECT id FROM users WHERE email = '${ASSIGNED_TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}'), ` + `NOW(), NOW() ` + `ON CONFLICT DO NOTHING` ); // Link replacement to the class/subject runSql( `INSERT INTO replacement_classes (replacement_id, class_id, subject_id) ` + `SELECT tr.id, '${classId}', '${subjectId}' ` + `FROM teacher_replacements tr ` + `JOIN users u ON tr.replacement_teacher_id = u.id ` + `WHERE u.email = '${REPLACEMENT_TEACHER_EMAIL}' AND tr.tenant_id = '${TENANT_ID}' AND tr.status = 'active' ` + `ON CONFLICT DO NOTHING` ); clearCache(); }); test('AC1: assigned teacher can access grade grid', async ({ page }) => { await loginAs(page, ASSIGNED_TEACHER_EMAIL); await page.goto(`${ALPHA_URL}/dashboard/teacher/evaluations/${evaluationId}/grades`); // Should see the grade grid with at least one student const gradeGrid = page.locator('.grade-grid'); await expect(gradeGrid).toBeVisible({ timeout: 15000 }); // Should see student name await expect(page.locator('.student-name')).toHaveCount(1); }); test('AC1: unassigned teacher cannot access grade grid', async ({ page }) => { await loginAs(page, UNASSIGNED_TEACHER_EMAIL); // Navigate to the evaluation's grade page await page.goto(`${ALPHA_URL}/dashboard/teacher/evaluations/${evaluationId}/grades`); // Backend returns 403, frontend shows "Évaluation non trouvée" await expect(page.getByText(/non trouvée/i)).toBeVisible({ timeout: 15000 }); // Grade grid should NOT be visible await expect(page.locator('.grade-grid')).not.toBeVisible(); }); test('AC1: active replacement teacher can access grade grid', async ({ page }) => { await loginAs(page, REPLACEMENT_TEACHER_EMAIL); await page.goto(`${ALPHA_URL}/dashboard/teacher/evaluations/${evaluationId}/grades`); // Should see the grade grid const gradeGrid = page.locator('.grade-grid'); await expect(gradeGrid).toBeVisible({ timeout: 15000 }); // Should see student name await expect(page.locator('.student-name')).toHaveCount(1); }); test('AC1: expired replacement teacher cannot access grade grid', async ({ page }) => { // Terminate the replacement to simulate expiry runSql( `UPDATE teacher_replacements SET status = 'ended', end_date = NOW() - INTERVAL '1 day', ended_at = NOW(), updated_at = NOW() ` + `WHERE replacement_teacher_id = (SELECT id FROM users WHERE email = '${REPLACEMENT_TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') ` + `AND tenant_id = '${TENANT_ID}'` ); clearCache(); await loginAs(page, REPLACEMENT_TEACHER_EMAIL); await page.goto(`${ALPHA_URL}/dashboard/teacher/evaluations/${evaluationId}/grades`); // Backend returns 403, frontend shows "Évaluation non trouvée" await expect(page.getByText(/non trouvée/i)).toBeVisible({ timeout: 15000 }); // Grade grid should NOT be visible await expect(page.locator('.grade-grid')).not.toBeVisible(); }); test.afterAll(async () => { // Restaurer le remplacement pour ne pas polluer les autres tests runSql( `UPDATE teacher_replacements SET status = 'active', end_date = NOW() + INTERVAL '7 days', ended_at = NULL, updated_at = NOW() ` + `WHERE replacement_teacher_id = (SELECT id FROM users WHERE email = '${REPLACEMENT_TEACHER_EMAIL}' AND tenant_id = '${TENANT_ID}') ` + `AND tenant_id = '${TENANT_ID}'` ); clearCache(); }); });