fix(ci): Corriger les tests E2E en CI
Plusieurs problèmes empêchaient les tests E2E de passer en CI : 1. Healthcheck : L'endpoint /api nécessite une authentification et retournait 401, causant l'échec du healthcheck. Remplacé par /api/docs qui est public. 2. Mailer : L'activation de compte déclenche l'envoi d'un email via mailpit, qui n'est pas disponible en CI. Ajout d'une variable d'environnement MAILER_DSN=null://null pour désactiver l'envoi. 3. Token partagé : Chaque navigateur (chromium, firefox, webkit) consommait le même token, causant des échecs pour les suivants. Maintenant chaque navigateur crée son propre token dans beforeAll avec un email unique (e2e-{browser}@example.com). 4. Nettoyage : Suppression de test-utils.ts et global-setup simplifié car la création de token est maintenant dans le fichier de test.
This commit is contained in:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -172,14 +172,18 @@ jobs:
|
|||||||
# Build images first (with Docker layer caching)
|
# Build images first (with Docker layer caching)
|
||||||
docker compose build php
|
docker compose build php
|
||||||
# Start services (includes db, redis, rabbitmq dependencies)
|
# Start services (includes db, redis, rabbitmq dependencies)
|
||||||
|
# Use null mailer transport since mailpit is not available in CI
|
||||||
docker compose up -d php
|
docker compose up -d php
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
env:
|
||||||
|
MAILER_DSN: "null://null"
|
||||||
|
|
||||||
- name: Wait for backend to be ready
|
- name: Wait for backend to be ready
|
||||||
run: |
|
run: |
|
||||||
echo "Waiting for backend to be ready (composer install + app startup)..."
|
echo "Waiting for backend to be ready (composer install + app startup)..."
|
||||||
# Wait up to 5 minutes for the backend to respond
|
# 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..."
|
echo "Waiting for backend..."
|
||||||
sleep 5
|
sleep 5
|
||||||
done'
|
done'
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rabbitmq:5672/%2f/messages
|
MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rabbitmq:5672/%2f/messages
|
||||||
MERCURE_URL: http://mercure/.well-known/mercure
|
MERCURE_URL: http://mercure/.well-known/mercure
|
||||||
MEILISEARCH_URL: http://meilisearch:7700
|
MEILISEARCH_URL: http://meilisearch:7700
|
||||||
MAILER_DSN: smtp://mailpit:1025
|
MAILER_DSN: ${MAILER_DSN:-smtp://mailpit:1025}
|
||||||
ports:
|
ports:
|
||||||
- "18000:8000" # Port externe 18000 pour eviter conflit
|
- "18000:8000" # Port externe 18000 pour eviter conflit
|
||||||
volumes:
|
volumes:
|
||||||
@@ -38,7 +38,7 @@ services:
|
|||||||
rabbitmq:
|
rabbitmq:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api"]
|
test: ["CMD", "curl", "-f", "http://localhost:8000/api/docs"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
@@ -1,5 +1,48 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
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('Account Activation Flow', () => {
|
||||||
test.describe('Token Validation', () => {
|
test.describe('Token Validation', () => {
|
||||||
@@ -23,7 +66,7 @@ test.describe('Account Activation Flow', () => {
|
|||||||
|
|
||||||
test.describe('Password Form', () => {
|
test.describe('Password Form', () => {
|
||||||
test('validates password requirements in real-time', async ({ page }) => {
|
test('validates password requirements in real-time', async ({ page }) => {
|
||||||
const token = getTestToken();
|
const token = getToken();
|
||||||
await page.goto(`/activate/${token}`);
|
await page.goto(`/activate/${token}`);
|
||||||
|
|
||||||
// Wait for form to be visible (token must be valid)
|
// 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 }) => {
|
test('requires password confirmation to match', async ({ page }) => {
|
||||||
const token = getTestToken();
|
const token = getToken();
|
||||||
await page.goto(`/activate/${token}`);
|
await page.goto(`/activate/${token}`);
|
||||||
|
|
||||||
const form = page.locator('form');
|
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 }) => {
|
test('submit button is disabled until form is valid', async ({ page }) => {
|
||||||
const token = getTestToken();
|
const token = getToken();
|
||||||
await page.goto(`/activate/${token}`);
|
await page.goto(`/activate/${token}`);
|
||||||
|
|
||||||
const form = page.locator('form');
|
const form = page.locator('form');
|
||||||
@@ -96,7 +139,7 @@ test.describe('Account Activation Flow', () => {
|
|||||||
|
|
||||||
test.describe('Establishment Info Display', () => {
|
test.describe('Establishment Info Display', () => {
|
||||||
test('shows establishment name and role when token is valid', async ({ page }) => {
|
test('shows establishment name and role when token is valid', async ({ page }) => {
|
||||||
const token = getTestToken();
|
const token = getToken();
|
||||||
await page.goto(`/activate/${token}`);
|
await page.goto(`/activate/${token}`);
|
||||||
|
|
||||||
const form = page.locator('form');
|
const form = page.locator('form');
|
||||||
@@ -111,7 +154,7 @@ test.describe('Account Activation Flow', () => {
|
|||||||
|
|
||||||
test.describe('Password Visibility Toggle', () => {
|
test.describe('Password Visibility Toggle', () => {
|
||||||
test('toggles password visibility', async ({ page }) => {
|
test('toggles password visibility', async ({ page }) => {
|
||||||
const token = getTestToken();
|
const token = getToken();
|
||||||
await page.goto(`/activate/${token}`);
|
await page.goto(`/activate/${token}`);
|
||||||
|
|
||||||
const form = page.locator('form');
|
const form = page.locator('form');
|
||||||
@@ -135,21 +178,31 @@ test.describe('Account Activation Flow', () => {
|
|||||||
|
|
||||||
test.describe('Full Activation Flow', () => {
|
test.describe('Full Activation Flow', () => {
|
||||||
test('activates account and redirects to login', async ({ page }) => {
|
test('activates account and redirects to login', async ({ page }) => {
|
||||||
const token = getTestToken();
|
const token = getToken();
|
||||||
await page.goto(`/activate/${token}`);
|
await page.goto(`/activate/${token}`);
|
||||||
|
|
||||||
const form = page.locator('form');
|
const form = page.locator('form');
|
||||||
await expect(form).toBeVisible({ timeout: 5000 });
|
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
|
// Fill valid password
|
||||||
await page.locator('#password').fill('SecurePass123');
|
await page.locator('#password').fill('SecurePass123');
|
||||||
await page.locator('#passwordConfirmation').fill('SecurePass123');
|
await page.locator('#passwordConfirmation').fill('SecurePass123');
|
||||||
|
|
||||||
// Submit
|
// Wait for validation to complete - button should now be enabled
|
||||||
await page.getByRole('button', { name: /activer mon compte/i }).click();
|
await expect(submitButton).toBeEnabled({ timeout: 2000 });
|
||||||
|
|
||||||
// Should redirect to login with success message
|
// Submit and wait for navigation
|
||||||
await expect(page).toHaveURL(/\/login\?activated=true/);
|
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();
|
await expect(page.getByText(/compte a été activé avec succès/i)).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
* 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() {
|
async function globalSetup() {
|
||||||
console.warn('🌱 Seeding test activation token...');
|
console.warn('🎭 E2E Global setup - tokens are created per browser project');
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default globalSetup;
|
export default globalSetup;
|
||||||
|
|||||||
@@ -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.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user