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_PASSWORD = 'BlockingTest123'; const TARGET_PASSWORD = 'TargetUser123'; test.describe('User Blocking', () => { test.describe.configure({ mode: 'serial' }); // Per-browser unique emails to avoid cross-project race conditions // (parallel browser projects share the same database) let ADMIN_EMAIL: string; let TARGET_EMAIL: string; function clearCache() { const projectRoot = join(__dirname, '../..'); const composeFile = join(projectRoot, 'compose.yaml'); const pools = ['paginated_queries.cache', 'users.cache']; for (const pool of pools) { try { execSync( `docker compose -f "${composeFile}" exec -T php php bin/console cache:pool:clear ${pool} 2>&1`, { encoding: 'utf-8' } ); } catch { // Cache pool may not exist in all environments } } } // eslint-disable-next-line no-empty-pattern test.beforeAll(async ({}, testInfo) => { const browser = testInfo.project.name; ADMIN_EMAIL = `e2e-blocking-admin-${browser}@example.com`; TARGET_EMAIL = `e2e-blocking-target-${browser}@example.com`; 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 to be blocked 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' } ); // Ensure target user is unblocked before tests start (idempotent cleanup) try { execSync( `docker compose -f "${composeFile}" exec -T php php bin/console dbal:run-sql "UPDATE users SET statut = 'active', blocked_at = NULL, blocked_reason = NULL WHERE email = '${TARGET_EMAIL}'" 2>&1`, { encoding: 'utf-8' } ); } catch { // Ignore cleanup errors } clearCache(); }); async function loginAsAdmin(page: import('@playwright/test').Page) { await page.goto(`${ALPHA_URL}/login`); 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 }); } test('admin can block a user and sees blocked status', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); // Wait for users table to load await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 }); // Search for the target user (pagination may hide them beyond page 1) const searchInput = page.locator('input[type="search"]'); await searchInput.fill(TARGET_EMAIL); await page.waitForTimeout(1000); await page.waitForLoadState('networkidle'); // Find the target user row const targetRow = page.locator('tr', { has: page.locator(`text=${TARGET_EMAIL}`) }); await expect(targetRow).toBeVisible({ timeout: 10000 }); // Click "Bloquer" button and wait for modal (retry handles hydration timing) await expect(async () => { await targetRow.getByRole('button', { name: /bloquer/i }).click(); await expect(page.locator('#block-modal-title')).toBeVisible({ timeout: 2000 }); }).toPass({ timeout: 10000 }); // Fill in the reason await page.locator('#block-reason').fill('Comportement inapproprié en E2E'); // Confirm the block and wait for the API response const blockResponsePromise = page.waitForResponse( (resp) => resp.url().includes('/block') && resp.request().method() === 'POST' ); await page.getByRole('button', { name: /confirmer le blocage/i }).click(); const blockResponse = await blockResponsePromise; expect(blockResponse.status()).toBeLessThan(400); // Wait for the modal to close and status change to be reflected await expect(page.locator('#block-modal-title')).not.toBeVisible({ timeout: 5000 }); // Clear cache and reload to get fresh data (block action may not invalidate paginated cache) clearCache(); await page.reload(); await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 }); await page.locator('input[type="search"]').fill(TARGET_EMAIL); await page.waitForTimeout(1000); await page.waitForLoadState('networkidle'); const updatedRow = page.locator('tr', { has: page.locator(`text=${TARGET_EMAIL}`) }); await expect(updatedRow).toBeVisible({ timeout: 10000 }); await expect(updatedRow.locator('.status-blocked')).toContainText('Suspendu', { timeout: 10000 }); // Verify the reason is displayed await expect(updatedRow.locator('.blocked-reason')).toContainText('Comportement inapproprié en E2E'); }); test('admin can unblock a suspended user', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 }); // Search for the target user (pagination may hide them beyond page 1) const searchInput = page.locator('input[type="search"]'); await searchInput.fill(TARGET_EMAIL); await page.waitForTimeout(1000); await page.waitForLoadState('networkidle'); // Find the suspended target user row const targetRow = page.locator('tr', { has: page.locator(`text=${TARGET_EMAIL}`) }); await expect(targetRow).toBeVisible({ timeout: 10000 }); // "Débloquer" button should be visible for suspended user const unblockButton = targetRow.getByRole('button', { name: /débloquer/i }); await expect(unblockButton).toBeVisible(); // Click unblock await unblockButton.click(); // Wait for the success message await expect(page.locator('.alert-success')).toBeVisible({ timeout: 10000 }); // Verify the user status changed back to "Actif" const updatedRow = page.locator('tr', { has: page.locator(`text=${TARGET_EMAIL}`) }); await expect(updatedRow.locator('.status-active')).toContainText('Actif'); }); test('blocked user sees specific error on login', async ({ page }) => { // First, block the user again await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 }); // Search for the target user (pagination may hide them beyond page 1) const searchInput = page.locator('input[type="search"]'); await searchInput.fill(TARGET_EMAIL); await page.waitForTimeout(1000); await page.waitForLoadState('networkidle'); const targetRow = page.locator('tr', { has: page.locator(`text=${TARGET_EMAIL}`) }); await expect(async () => { await targetRow.getByRole('button', { name: /bloquer/i }).click(); await expect(page.locator('#block-modal-title')).toBeVisible({ timeout: 2000 }); }).toPass({ timeout: 10000 }); await page.locator('#block-reason').fill('Bloqué pour test login'); const blockResponsePromise = page.waitForResponse( (resp) => resp.url().includes('/block') && resp.request().method() === 'POST' ); await page.getByRole('button', { name: /confirmer le blocage/i }).click(); const blockResponse = await blockResponsePromise; expect(blockResponse.status()).toBeLessThan(400); await expect(page.locator('.alert-success')).toBeVisible({ timeout: 10000 }); // Logout await page.getByRole('button', { name: /déconnexion/i }).click(); // Try to log in as the blocked user await page.goto(`${ALPHA_URL}/login`); await page.locator('#email').fill(TARGET_EMAIL); await page.locator('#password').fill(TARGET_PASSWORD); await page.getByRole('button', { name: /se connecter/i }).click(); // Should see a suspended account error, not the generic credentials error const errorBanner = page.locator('.error-banner.account-suspended'); await expect(errorBanner).toBeVisible({ timeout: 5000 }); await expect(errorBanner).toContainText(/suspendu|contactez/i); }); test('admin cannot block themselves', async ({ page }) => { await loginAsAdmin(page); await page.goto(`${ALPHA_URL}/admin/users`); await expect(page.locator('.users-table')).toBeVisible({ timeout: 10000 }); // Search for the admin user (pagination may hide them beyond page 1) const searchInput = page.locator('input[type="search"]'); await searchInput.fill(ADMIN_EMAIL); await page.waitForTimeout(1000); await page.waitForLoadState('networkidle'); // Find the admin's own row const adminRow = page.locator('tr', { has: page.locator(`text=${ADMIN_EMAIL}`) }); await expect(adminRow).toBeVisible({ timeout: 15000 }); // "Bloquer" button should NOT be present on the admin's own row await expect(adminRow.getByRole('button', { name: /^bloquer$/i })).not.toBeVisible(); }); });