Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
129 lines
4.7 KiB
TypeScript
129 lines
4.7 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);
|
|
|
|
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 ADMIN_EMAIL = 'e2e-actlink-admin@example.com';
|
|
const ADMIN_PASSWORD = 'ActLinkTest123';
|
|
const STUDENT_EMAIL = 'e2e-actlink-student@example.com';
|
|
const STUDENT_PASSWORD = 'StudentTest123';
|
|
const UNIQUE_SUFFIX = Date.now();
|
|
const PARENT_EMAIL = `e2e-actlink-parent-${UNIQUE_SUFFIX}@example.com`;
|
|
const PARENT_PASSWORD = 'ParentActivation1!';
|
|
|
|
let studentUserId: string;
|
|
let activationToken: string;
|
|
|
|
function extractUserId(output: string): string {
|
|
const match = output.match(/User ID\s+([a-f0-9-]{36})/i);
|
|
if (!match) {
|
|
throw new Error(`Could not extract User ID from command output:\n${output}`);
|
|
}
|
|
return match[1];
|
|
}
|
|
|
|
test.describe('Activation with Parent-Child Auto-Link', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.beforeAll(async () => {
|
|
const projectRoot = join(__dirname, '../..');
|
|
const composeFile = join(projectRoot, 'compose.yaml');
|
|
|
|
const run = (cmd: string) => {
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
try {
|
|
return execSync(cmd, { encoding: 'utf-8' });
|
|
} catch (e) {
|
|
if (attempt === 2) throw e;
|
|
execSync('sleep 2');
|
|
}
|
|
}
|
|
return '';
|
|
};
|
|
|
|
// Create admin user
|
|
run(
|
|
`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`
|
|
);
|
|
|
|
// Create student user and capture userId
|
|
const studentOutput = run(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${STUDENT_EMAIL} --password=${STUDENT_PASSWORD} --role=ROLE_ELEVE 2>&1`
|
|
);
|
|
studentUserId = extractUserId(studentOutput);
|
|
|
|
// Clean up any existing guardian links for this student
|
|
try {
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "DELETE FROM student_guardians WHERE student_id = '${studentUserId}'" 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
|
|
// Create activation token for parent WITH student-id for auto-linking
|
|
const tokenOutput = execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-activation-token --email=${PARENT_EMAIL} --role=PARENT --tenant=ecole-alpha --student-id=${studentUserId} 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
|
|
const tokenMatch = tokenOutput.match(/Token\s+([a-f0-9-]{36})/i);
|
|
if (!tokenMatch) {
|
|
throw new Error(`Could not extract token from command output:\n${tokenOutput}`);
|
|
}
|
|
activationToken = tokenMatch[1];
|
|
});
|
|
|
|
test('[P1] should activate parent account and auto-link to student', async ({ page }) => {
|
|
// Navigate to the activation page
|
|
await page.goto(`${ALPHA_URL}/activate/${activationToken}`);
|
|
|
|
// Wait for the activation form to load
|
|
await expect(page.locator('#password')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Fill the password form
|
|
await page.locator('#password').fill(PARENT_PASSWORD);
|
|
await page.locator('#passwordConfirmation').fill(PARENT_PASSWORD);
|
|
|
|
// Wait for validation to pass and submit
|
|
const submitButton = page.getByRole('button', { name: /activer mon compte/i });
|
|
await expect(submitButton).toBeEnabled({ timeout: 5000 });
|
|
await submitButton.click();
|
|
|
|
// Should redirect to login with activated=true
|
|
await page.waitForURL(/\/login\?activated=true/, { timeout: 15000 });
|
|
|
|
// Now login as admin to verify the auto-link
|
|
await page.locator('#email').fill(ADMIN_EMAIL);
|
|
await page.locator('#password').fill(ADMIN_PASSWORD);
|
|
await Promise.all([
|
|
page.waitForURL(/\/dashboard/, { timeout: 60000 }),
|
|
page.getByRole('button', { name: /se connecter/i }).click()
|
|
]);
|
|
|
|
// Navigate to the student's page to check guardian list
|
|
await page.goto(`${ALPHA_URL}/admin/students/${studentUserId}`);
|
|
|
|
// Wait for the guardian section to load
|
|
await expect(page.locator('.guardian-section')).toBeVisible({ timeout: 10000 });
|
|
await expect(
|
|
page.locator('.guardian-list')
|
|
).toBeVisible({ timeout: 10000 });
|
|
|
|
// The auto-linked parent should appear in the guardian list
|
|
const guardianItem = page.locator('.guardian-item').first();
|
|
await expect(guardianItem).toBeVisible();
|
|
// Auto-linking uses RelationshipType::OTHER → label "Autre"
|
|
await expect(guardianItem).toContainText('Autre');
|
|
});
|
|
});
|