diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e11b883..8f44358 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,14 +172,18 @@ jobs: # Build images first (with Docker layer caching) docker compose build php # Start services (includes db, redis, rabbitmq dependencies) + # Use null mailer transport since mailpit is not available in CI docker compose up -d php timeout-minutes: 10 + env: + MAILER_DSN: "null://null" - name: Wait for backend to be ready run: | echo "Waiting for backend to be ready (composer install + app startup)..." # Wait up to 5 minutes for the backend to respond - timeout 300 bash -c 'until curl -sf http://localhost:18000/api > /dev/null 2>&1; do + # Using /api/docs which is a public endpoint (no auth required) + timeout 300 bash -c 'until curl -sf http://localhost:18000/api/docs > /dev/null 2>&1; do echo "Waiting for backend..." sleep 5 done' diff --git a/compose.yaml b/compose.yaml index 7f0962c..8c0f8b1 100644 --- a/compose.yaml +++ b/compose.yaml @@ -23,7 +23,7 @@ services: MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rabbitmq:5672/%2f/messages MERCURE_URL: http://mercure/.well-known/mercure MEILISEARCH_URL: http://meilisearch:7700 - MAILER_DSN: smtp://mailpit:1025 + MAILER_DSN: ${MAILER_DSN:-smtp://mailpit:1025} ports: - "18000:8000" # Port externe 18000 pour eviter conflit volumes: @@ -38,7 +38,7 @@ services: rabbitmq: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/api"] + test: ["CMD", "curl", "-f", "http://localhost:8000/api/docs"] interval: 10s timeout: 5s retries: 5 diff --git a/frontend/e2e/activation.spec.ts b/frontend/e2e/activation.spec.ts index 16cf37a..2871d5b 100644 --- a/frontend/e2e/activation.spec.ts +++ b/frontend/e2e/activation.spec.ts @@ -1,5 +1,48 @@ import { test, expect } from '@playwright/test'; -import { getTestToken } from './test-utils'; +import { execSync } from 'child_process'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Each browser project gets its own token to avoid conflicts +let testToken: string | null = null; + +// eslint-disable-next-line no-empty-pattern +test.beforeAll(async ({ }, testInfo) => { + const browserName = testInfo.project.name; + + // Create a unique token for this browser project + try { + const projectRoot = join(__dirname, '../..'); + const composeFile = join(projectRoot, 'compose.yaml'); + const email = `e2e-${browserName}@example.com`; + + const result = execSync( + `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-activation-token --email=${email} 2>&1`, + { encoding: 'utf-8' } + ); + + const tokenMatch = result.match(/Token\s+([a-f0-9-]{36})/i); + if (tokenMatch) { + testToken = tokenMatch[1]; + // eslint-disable-next-line no-console + console.warn(`[${browserName}] Test token created: ${testToken}`); + } else { + console.error(`[${browserName}] Could not extract token from output:`, result); + } + } catch (error) { + console.error(`[${browserName}] Failed to create test token:`, error); + } +}); + +function getToken(): string { + if (!testToken) { + throw new Error('No test token available. Make sure Docker is running.'); + } + return testToken; +} test.describe('Account Activation Flow', () => { test.describe('Token Validation', () => { @@ -23,7 +66,7 @@ test.describe('Account Activation Flow', () => { test.describe('Password Form', () => { test('validates password requirements in real-time', async ({ page }) => { - const token = getTestToken(); + const token = getToken(); await page.goto(`/activate/${token}`); // Wait for form to be visible (token must be valid) @@ -54,7 +97,7 @@ test.describe('Account Activation Flow', () => { }); test('requires password confirmation to match', async ({ page }) => { - const token = getTestToken(); + const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); @@ -74,7 +117,7 @@ test.describe('Account Activation Flow', () => { }); test('submit button is disabled until form is valid', async ({ page }) => { - const token = getTestToken(); + const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); @@ -96,7 +139,7 @@ test.describe('Account Activation Flow', () => { test.describe('Establishment Info Display', () => { test('shows establishment name and role when token is valid', async ({ page }) => { - const token = getTestToken(); + const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); @@ -111,7 +154,7 @@ test.describe('Account Activation Flow', () => { test.describe('Password Visibility Toggle', () => { test('toggles password visibility', async ({ page }) => { - const token = getTestToken(); + const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); @@ -135,21 +178,31 @@ test.describe('Account Activation Flow', () => { test.describe('Full Activation Flow', () => { test('activates account and redirects to login', async ({ page }) => { - const token = getTestToken(); + const token = getToken(); await page.goto(`/activate/${token}`); const form = page.locator('form'); await expect(form).toBeVisible({ timeout: 5000 }); + const submitButton = page.getByRole('button', { name: /activer mon compte/i }); + + // Button should be disabled initially (no password yet) + await expect(submitButton).toBeDisabled(); + // Fill valid password await page.locator('#password').fill('SecurePass123'); await page.locator('#passwordConfirmation').fill('SecurePass123'); - // Submit - await page.getByRole('button', { name: /activer mon compte/i }).click(); + // Wait for validation to complete - button should now be enabled + await expect(submitButton).toBeEnabled({ timeout: 2000 }); - // Should redirect to login with success message - await expect(page).toHaveURL(/\/login\?activated=true/); + // Submit and wait for navigation + await Promise.all([ + page.waitForURL(/\/login\?activated=true/, { timeout: 10000 }), + submitButton.click() + ]); + + // Verify success message await expect(page.getByText(/compte a été activé avec succès/i)).toBeVisible(); }); }); diff --git a/frontend/e2e/global-setup.ts b/frontend/e2e/global-setup.ts index f88fe81..04844f8 100644 --- a/frontend/e2e/global-setup.ts +++ b/frontend/e2e/global-setup.ts @@ -1,52 +1,12 @@ -import { execSync } from 'child_process'; -import { writeFileSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - /** * Global setup for E2E tests. - * Seeds a test activation token before tests run. + * + * Note: Token creation is now handled per-browser in the test files + * using beforeAll hooks. This ensures each browser project gets its + * own unique token that won't be consumed by other browsers. */ async function globalSetup() { - console.warn('🌱 Seeding test activation token...'); - - try { - // Call the backend command to create a test token - // Project root is 2 levels up from frontend/e2e/ - const projectRoot = join(__dirname, '../..'); - const composeFile = join(projectRoot, 'compose.yaml'); - - const result = execSync( - `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-activation-token --email=e2e-test@example.com 2>&1`, - { - encoding: 'utf-8' - } - ); - - // Extract the token from the output - // Output format: "Token f9174245-9766-4ef1-b6e9-a6795aa2da04" - const tokenMatch = result.match(/Token\s+([a-f0-9-]{36})/i); - if (!tokenMatch) { - console.error('❌ Could not extract token from output:', result); - throw new Error('Failed to extract token from command output'); - } - - const token = tokenMatch[1]; - console.warn(`✅ Test token created: ${token}`); - - // Write the token to a file for tests to use - const tokenFile = join(__dirname, '.test-token'); - writeFileSync(tokenFile, token); - - console.warn('✅ Token saved to .test-token file'); - } catch (error) { - console.error('❌ Failed to seed test token:', error); - // Don't throw - tests can still run with skipped token-dependent tests - console.warn('⚠️ Tests requiring valid tokens will be skipped'); - } + console.warn('🎭 E2E Global setup - tokens are created per browser project'); } export default globalSetup; diff --git a/frontend/e2e/test-utils.ts b/frontend/e2e/test-utils.ts deleted file mode 100644 index b596246..0000000 --- a/frontend/e2e/test-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { readFileSync, existsSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -/** - * Get the seeded test token. - * The token is created by global-setup.ts before tests run via Docker. - */ -export function getTestToken(): string { - const tokenFile = join(__dirname, '.test-token'); - - if (existsSync(tokenFile)) { - return readFileSync(tokenFile, 'utf-8').trim(); - } - - throw new Error( - 'No .test-token file found. Make sure Docker is running and global-setup.ts executed successfully.' - ); -}