Files
Classeo/frontend/e2e/super-admin-provisioning.spec.ts
Mathias STRASSER dc2be898d5
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled
feat: Provisionner automatiquement un nouvel établissement
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.
2026-04-16 09:27:25 +02:00

206 lines
7.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 projectRoot = join(__dirname, '../..');
const composeFile = join(projectRoot, 'compose.yaml');
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 SA_PASSWORD = 'SuperAdmin123';
const UNIQUE_SUFFIX = Date.now();
function getSuperAdminEmail(browserName: string): string {
return `e2e-prov-sa-${browserName}@test.com`;
}
// eslint-disable-next-line no-empty-pattern
test.beforeAll(async ({}, testInfo) => {
const browserName = testInfo.project.name;
const saEmail = getSuperAdminEmail(browserName);
try {
execSync(
`docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-super-admin --email=${saEmail} --password=${SA_PASSWORD} 2>&1`,
{ encoding: 'utf-8' }
);
} catch (error) {
console.error(`[${browserName}] Failed to create super admin:`, error);
}
});
async function loginAsSuperAdmin(
page: import('@playwright/test').Page,
email: string
) {
await page.goto(`${ALPHA_URL}/login`);
await expect(page.getByRole('heading', { name: /connexion/i })).toBeVisible();
await page.locator('#email').fill(email);
await page.locator('#password').fill(SA_PASSWORD);
const submitButton = page.getByRole('button', { name: /se connecter/i });
await Promise.all([
page.waitForURL('**/super-admin/dashboard', { timeout: 30000 }),
submitButton.click()
]);
}
async function navigateToEstablishments(page: import('@playwright/test').Page) {
const link = page.getByRole('link', { name: /établissements/i });
await Promise.all([
page.waitForURL('**/super-admin/establishments', { timeout: 10000 }),
link.click()
]);
await expect(page.getByRole('heading', { name: /établissements/i })).toBeVisible({ timeout: 10000 });
}
async function navigateToCreateForm(page: import('@playwright/test').Page) {
const newLink = page.getByRole('link', { name: /nouvel établissement/i });
await expect(newLink).toBeVisible({ timeout: 10000 });
await Promise.all([
page.waitForURL('**/super-admin/establishments/new', { timeout: 10000 }),
newLink.click()
]);
await expect(page.locator('#name')).toBeVisible({ timeout: 10000 });
}
test.describe('Establishment Provisioning (Story 2-17) [P1]', () => {
test.describe.configure({ mode: 'serial' });
test.describe('Subdomain Auto-generation', () => {
test('[P1] typing name auto-generates subdomain', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
await navigateToCreateForm(page);
await page.locator('#name').fill('École Saint-Exupéry');
// Subdomain should be auto-generated: accents removed, spaces→hyphens, lowercase
await expect(page.locator('#subdomain')).toHaveValue('ecole-saint-exupery');
});
test('[P2] subdomain suffix .classeo.fr is displayed', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
await navigateToCreateForm(page);
await expect(page.locator('.subdomain-suffix')).toHaveText('.classeo.fr');
});
});
test.describe('Create Establishment Flow', () => {
const establishmentName = `E2E Test ${UNIQUE_SUFFIX}`;
const adminEmailForEstab = `admin-prov-${UNIQUE_SUFFIX}@test.com`;
test('[P1] submitting form creates establishment and redirects to list', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
await navigateToCreateForm(page);
// Fill in the form
await page.locator('#name').fill(establishmentName);
await page.locator('#adminEmail').fill(adminEmailForEstab);
// Subdomain should be auto-generated
const subdomain = await page.locator('#subdomain').inputValue();
expect(subdomain.length).toBeGreaterThan(0);
// Submit
const submitButton = page.getByRole('button', { name: /créer l'établissement/i });
await expect(submitButton).toBeEnabled();
const apiResponsePromise = page.waitForResponse(
(resp) => resp.url().includes('/super-admin/establishments') && resp.request().method() === 'POST'
);
await submitButton.click();
// Verify API returns establishment in provisioning status
const apiResponse = await apiResponsePromise;
expect(apiResponse.status()).toBeLessThan(400);
const body = await apiResponse.json();
expect(body.status).toBe('provisioning');
// Should redirect back to establishments list
await page.waitForURL('**/super-admin/establishments', { timeout: 15000 });
await expect(page.getByRole('heading', { name: /établissements/i })).toBeVisible({ timeout: 10000 });
});
test('[P1] created establishment appears in the list', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
// The establishment created in previous test should be visible
await expect(page.locator('table')).toBeVisible({ timeout: 10000 });
await expect(page.locator('td', { hasText: establishmentName })).toBeVisible({ timeout: 10000 });
});
test('[P1] created establishment has a visible status badge', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
// Find the row for our establishment
const row = page.locator('tr', { has: page.locator(`text=${establishmentName}`) });
await expect(row).toBeVisible({ timeout: 10000 });
// Status badge should be visible (provisioning status already verified via API response in creation test)
const badge = row.locator('.badge');
await expect(badge).toBeVisible();
await expect(badge).not.toHaveText('');
});
});
test.describe('Form Validation', () => {
test('[P2] submit button disabled with empty fields', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
await navigateToCreateForm(page);
const submitButton = page.getByRole('button', { name: /créer l'établissement/i });
await expect(submitButton).toBeDisabled();
});
test('[P2] submit button enabled when all fields filled', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
await navigateToCreateForm(page);
await page.locator('#name').fill('Test School');
await page.locator('#adminEmail').fill('admin@test.com');
const submitButton = page.getByRole('button', { name: /créer l'établissement/i });
await expect(submitButton).toBeEnabled();
});
test('[P2] cancel button returns to establishments list', async ({ page }, testInfo) => {
const email = getSuperAdminEmail(testInfo.project.name);
await loginAsSuperAdmin(page, email);
await navigateToEstablishments(page);
await navigateToCreateForm(page);
const cancelLink = page.getByRole('link', { name: /annuler/i });
await Promise.all([
page.waitForURL('**/super-admin/establishments', { timeout: 10000 }),
cancelLink.click()
]);
await expect(page.getByRole('heading', { name: /établissements/i })).toBeVisible();
});
});
});