Lorsqu'un super-admin crée un établissement via l'interface, le système doit automatiquement créer la base tenant, exécuter les migrations, créer le premier utilisateur admin et envoyer l'invitation — le tout de manière asynchrone pour ne pas bloquer la réponse HTTP. Ce mécanisme rend chaque établissement opérationnel dès sa création sans intervention manuelle sur l'infrastructure.
190 lines
8.4 KiB
TypeScript
190 lines
8.4 KiB
TypeScript
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();
|
|
});
|
|
});
|