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.
167 lines
6.9 KiB
TypeScript
167 lines
6.9 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-roles-admin@example.com';
|
|
const ADMIN_PASSWORD = 'RolesAdmin123';
|
|
const TARGET_EMAIL = `e2e-roles-target-${Date.now()}@example.com`;
|
|
const TARGET_PASSWORD = 'RolesTarget123';
|
|
|
|
test.describe('Multi-Role Assignment (FR5) [P2]', () => {
|
|
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 target user with single role (PROF)
|
|
execSync(
|
|
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=${TARGET_EMAIL} --password=${TARGET_PASSWORD} --role=ROLE_PROF 2>&1`,
|
|
{ encoding: 'utf-8' }
|
|
);
|
|
});
|
|
|
|
async function loginAsAdmin(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/login`);
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.locator('#email').fill(ADMIN_EMAIL);
|
|
await page.locator('#password').fill(ADMIN_PASSWORD);
|
|
await page.getByRole('button', { name: /se connecter/i }).click();
|
|
await page.waitForURL(/\/dashboard/, { timeout: 60000 });
|
|
}
|
|
|
|
async function openRolesModalForTarget(page: import('@playwright/test').Page) {
|
|
await page.goto(`${ALPHA_URL}/admin/users`);
|
|
await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 });
|
|
|
|
// Search for the target user (paginated list may not show them on page 1)
|
|
await page.getByRole('searchbox').fill(TARGET_EMAIL);
|
|
await page.waitForTimeout(500); // debounce
|
|
|
|
// Find the target user row and click "Rôles" button
|
|
const targetRow = page.locator('tr', { has: page.locator(`text=${TARGET_EMAIL}`) });
|
|
await expect(targetRow).toBeVisible({ timeout: 10000 });
|
|
await targetRow.getByRole('button', { name: 'Rôles' }).click();
|
|
|
|
// Modal should appear
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
await expect(page.locator('#roles-modal-title')).toHaveText('Modifier les rôles');
|
|
}
|
|
|
|
test('[P2] admin can open role modal showing current roles', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await openRolesModalForTarget(page);
|
|
|
|
// Target user email should be displayed in modal
|
|
await expect(page.locator('.roles-modal-user')).toContainText(TARGET_EMAIL);
|
|
|
|
// ROLE_PROF should be checked (current role)
|
|
const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]');
|
|
await expect(profCheckbox).toBeChecked();
|
|
|
|
// Other roles should be unchecked
|
|
const adminCheckbox = page.locator('.role-checkbox-label', { hasText: 'Directeur' }).locator('input[type="checkbox"]');
|
|
await expect(adminCheckbox).not.toBeChecked();
|
|
});
|
|
|
|
test('[P2] admin can assign multiple roles to a user', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await openRolesModalForTarget(page);
|
|
|
|
// Add Vie Scolaire role in addition to PROF
|
|
const vieScolaireLabel = page.locator('.role-checkbox-label', { hasText: 'Vie Scolaire' });
|
|
await vieScolaireLabel.locator('input[type="checkbox"]').check();
|
|
|
|
// Both should now be checked
|
|
const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]');
|
|
await expect(profCheckbox).toBeChecked();
|
|
await expect(vieScolaireLabel.locator('input[type="checkbox"]')).toBeChecked();
|
|
|
|
// Save
|
|
const saveResponsePromise = page.waitForResponse(
|
|
(resp) => resp.url().includes('/roles') && resp.request().method() === 'PUT'
|
|
);
|
|
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
|
const saveResponse = await saveResponsePromise;
|
|
expect(saveResponse.status()).toBeLessThan(400);
|
|
|
|
// Success message should appear
|
|
await expect(page.locator('.alert-success')).toContainText(/rôles.*mis à jour/i, { timeout: 5000 });
|
|
});
|
|
|
|
test('[P2] assigned roles persist after page reload', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await openRolesModalForTarget(page);
|
|
|
|
// Both PROF and VIE_SCOLAIRE should still be checked after reload
|
|
const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]');
|
|
const vieScolaireCheckbox = page.locator('.role-checkbox-label', { hasText: 'Vie Scolaire' }).locator('input[type="checkbox"]');
|
|
|
|
await expect(profCheckbox).toBeChecked();
|
|
await expect(vieScolaireCheckbox).toBeChecked();
|
|
});
|
|
|
|
test('[P2] admin can remove a role while keeping at least one', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await openRolesModalForTarget(page);
|
|
|
|
// Uncheck Vie Scolaire (added in previous test)
|
|
const vieScolaireCheckbox = page.locator('.role-checkbox-label', { hasText: 'Vie Scolaire' }).locator('input[type="checkbox"]');
|
|
await vieScolaireCheckbox.uncheck();
|
|
|
|
// PROF should still be checked
|
|
const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]');
|
|
await expect(profCheckbox).toBeChecked();
|
|
await expect(vieScolaireCheckbox).not.toBeChecked();
|
|
|
|
// Save
|
|
const saveResponsePromise = page.waitForResponse(
|
|
(resp) => resp.url().includes('/roles') && resp.request().method() === 'PUT'
|
|
);
|
|
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
|
await saveResponsePromise;
|
|
|
|
await expect(page.locator('.alert-success')).toContainText(/rôles.*mis à jour/i, { timeout: 5000 });
|
|
});
|
|
|
|
test('[P2] last role checkbox is disabled to prevent removal', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await openRolesModalForTarget(page);
|
|
|
|
// Only PROF should be checked now (after previous test removed VIE_SCOLAIRE)
|
|
const profCheckbox = page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('input[type="checkbox"]');
|
|
await expect(profCheckbox).toBeChecked();
|
|
|
|
// Last role checkbox should be disabled
|
|
await expect(profCheckbox).toBeDisabled();
|
|
|
|
// "(dernier rôle)" hint should be visible
|
|
await expect(
|
|
page.locator('.role-checkbox-label', { hasText: 'Enseignant' }).locator('.role-checkbox-hint')
|
|
).toContainText('dernier rôle');
|
|
});
|
|
|
|
test('[P2] role modal can be closed with Escape', async ({ page }) => {
|
|
await loginAsAdmin(page);
|
|
await openRolesModalForTarget(page);
|
|
|
|
await page.getByRole('dialog').press('Escape');
|
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
|
});
|
|
});
|