test(e2e) : initialise la suite Playwright (login + sidebar RBAC)
- 11 tests couvrant le login (3) et la visibilite sidebar par RBAC (8) - 6 personas seedes via la commande app:seed-e2e, miroir cote front dans frontend/tests/e2e/_fixtures/personas.ts - Page Objects (LoginPage, SidebarComponent) avec selecteurs stables par href + loginAs programmatique via cookie BEARER - Targets Makefile : seed-e2e, test-e2e, test-e2e-ui, install-e2e-deps - CLAUDE.md + README.md : workflow E2E + regle d'or "un E2E par bug prod uniquement" pour garder la suite maintenable dans la duree Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -29,6 +29,13 @@ frontend/.output/
|
|||||||
frontend/dist/
|
frontend/dist/
|
||||||
###< frontend ###
|
###< frontend ###
|
||||||
|
|
||||||
|
###> playwright ###
|
||||||
|
frontend/test-results/
|
||||||
|
frontend/playwright-report/
|
||||||
|
frontend/blob-report/
|
||||||
|
frontend/playwright/.cache/
|
||||||
|
###< playwright ###
|
||||||
|
|
||||||
###> docker ###
|
###> docker ###
|
||||||
infra/dev/.env.docker.local
|
infra/dev/.env.docker.local
|
||||||
###< docker ###
|
###< docker ###
|
||||||
|
|||||||
31
CLAUDE.md
31
CLAUDE.md
@@ -175,6 +175,7 @@ make migration-migrate # Lancer les migrations
|
|||||||
make fixtures # Charger les fixtures
|
make fixtures # Charger les fixtures
|
||||||
make db-reset # Reset BDD + migrations + fixtures
|
make db-reset # Reset BDD + migrations + fixtures
|
||||||
make test # PHPUnit
|
make test # PHPUnit
|
||||||
|
make nuxt-test # Vitest (tests unitaires frontend)
|
||||||
make php-cs-fixer-allow-risky # Fix code style PHP
|
make php-cs-fixer-allow-risky # Fix code style PHP
|
||||||
make logs-dev # Tail logs Symfony
|
make logs-dev # Tail logs Symfony
|
||||||
```
|
```
|
||||||
@@ -185,6 +186,36 @@ docker exec -t -u root php-coltura-fpm chown -R www-data:www-data /var/www/html/
|
|||||||
docker exec -t -u www-data php-coltura-fpm php bin/console cache:clear
|
docker exec -t -u www-data php-coltura-fpm php bin/console cache:clear
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Tests E2E (Playwright)
|
||||||
|
|
||||||
|
La suite E2E vit dans `frontend/tests/e2e/` (personas, Page Objects, specs). Elle tourne sur l'host (Playwright a besoin d'un navigateur reel, pas dans un container PHP).
|
||||||
|
|
||||||
|
**Bootstrap one-time par poste de dev :**
|
||||||
|
```bash
|
||||||
|
make install-e2e-deps # Telecharge Chromium + installe les libs systeme (sudo)
|
||||||
|
```
|
||||||
|
A relancer uniquement si `@playwright/test` upgrade de version majeure.
|
||||||
|
|
||||||
|
**Workflow nominal :**
|
||||||
|
```bash
|
||||||
|
# Terminal 1
|
||||||
|
make start # Containers up
|
||||||
|
make seed-e2e # Cree les 6 personas e2e.* (idempotent)
|
||||||
|
make dev-nuxt # Dev server sur :3004 (garder ouvert)
|
||||||
|
|
||||||
|
# Terminal 2
|
||||||
|
make test-e2e # Run la suite
|
||||||
|
# ou
|
||||||
|
make test-e2e-ui # UI interactive pour debug
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regle d'or E2E** (a respecter pour garder la suite maintenable sur la duree) : un nouveau test E2E ne s'ajoute QUE quand un bug critique est passe en prod. Sinon, la bonne place est un test unitaire Vitest (plus rapide, plus stable). Cf. `frontend/tests/e2e/_fixtures/personas.ts` pour etendre la matrice RBAC via un persona existant plutot que d'ajouter un test.
|
||||||
|
|
||||||
|
**Etendre la matrice RBAC** : pour ajouter une permission testable, toucher les 3 endroits (sinon drift garanti) :
|
||||||
|
1. `config/sidebar.php` — attacher `permission` au bon item
|
||||||
|
2. `frontend/tests/e2e/_fixtures/personas.ts` — ajuster `permissions` + `expectedAdminLinks` d'un persona existant (ne pas creer de nouveau persona par reflexe)
|
||||||
|
3. `src/Module/Core/Infrastructure/Console/SeedE2ECommand.php` — miroir back du meme persona
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
### Commits
|
### Commits
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -46,10 +46,35 @@ make dev-nuxt # Port 3003
|
|||||||
| `make migration-migrate` | Lancer les migrations |
|
| `make migration-migrate` | Lancer les migrations |
|
||||||
| `make fixtures` | Charger les fixtures |
|
| `make fixtures` | Charger les fixtures |
|
||||||
| `make db-reset` | Reset BDD + migrations + fixtures |
|
| `make db-reset` | Reset BDD + migrations + fixtures |
|
||||||
| `make test` | PHPUnit |
|
| `make test` | PHPUnit (tests back) |
|
||||||
|
| `make nuxt-test` | Vitest (tests unitaires front) |
|
||||||
|
| `make test-e2e` | Playwright (tests E2E front) |
|
||||||
|
| `make test-e2e-ui` | Playwright UI interactive (debug) |
|
||||||
|
| `make seed-e2e` | Seed les 6 personas E2E |
|
||||||
|
| `make install-e2e-deps` | One-time : Chromium + libs systeme (sudo) |
|
||||||
| `make php-cs-fixer-allow-risky` | Fix code style PHP |
|
| `make php-cs-fixer-allow-risky` | Fix code style PHP |
|
||||||
| `make logs-dev` | Tail logs Symfony |
|
| `make logs-dev` | Tail logs Symfony |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- **Back** : `make test` (PHPUnit). Fixtures dediees sous `tests/Fixtures/`.
|
||||||
|
- **Front unitaire** : `make nuxt-test` (Vitest, happy-dom). Composables, utils, stores — rapide, <30s.
|
||||||
|
- **Front E2E** : `make test-e2e` (Playwright). Couvre login + matrice RBAC sidebar. Suite volontairement minimaliste (11 tests) — voir la regle d'or dans `CLAUDE.md`.
|
||||||
|
|
||||||
|
**Bootstrap E2E (une fois par poste)** :
|
||||||
|
```bash
|
||||||
|
make install-e2e-deps # Telecharge Chromium + libs systeme via apt (sudo)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workflow E2E** :
|
||||||
|
```bash
|
||||||
|
# Terminal 1 : containers + dev server
|
||||||
|
make start && make seed-e2e && make dev-nuxt
|
||||||
|
|
||||||
|
# Terminal 2 : tests
|
||||||
|
make test-e2e
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
**Modular Monolith DDD** : chaque module est un bounded context autonome, activable/desactivable par tenant. Le backend est la seule source de verite pour l'activation et l'organisation de la sidebar.
|
**Modular Monolith DDD** : chaque module est un bounded context autonome, activable/desactivable par tenant. Le backend est la seule source de verite pour l'activation et l'organisation de la sidebar.
|
||||||
|
|||||||
64
frontend/package-lock.json
generated
64
frontend/package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/eslint-config": "^1.9.0",
|
"@nuxt/eslint-config": "^1.9.0",
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||||
"@typescript-eslint/parser": "^8.44.1",
|
"@typescript-eslint/parser": "^8.44.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
@@ -3895,6 +3896,22 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@polka/url": {
|
"node_modules/@polka/url": {
|
||||||
"version": "1.0.0-next.29",
|
"version": "1.0.0-next.29",
|
||||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||||
@@ -12613,6 +12630,53 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pluralize": {
|
"node_modules/pluralize": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@malio/layer-ui": "^1.4.2",
|
"@malio/layer-ui": "^1.4.2",
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/eslint-config": "^1.9.0",
|
"@nuxt/eslint-config": "^1.9.0",
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||||
"@typescript-eslint/parser": "^8.44.1",
|
"@typescript-eslint/parser": "^8.44.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
|
|||||||
42
frontend/playwright.config.ts
Normal file
42
frontend/playwright.config.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config Playwright pour les tests E2E de Coltura.
|
||||||
|
*
|
||||||
|
* Pre-requis avant de lancer :
|
||||||
|
* 1. Les containers Docker tournent (`make start`)
|
||||||
|
* 2. Le dev server Nuxt est lance (`make dev-nuxt`) sur le port 3004
|
||||||
|
* 3. Les personas E2E sont seedes (`make seed-e2e` — cf. SeedE2ECommand cote back)
|
||||||
|
*
|
||||||
|
* La baseURL cible le dev server Nuxt (HMR) en dev local ; surcharger avec
|
||||||
|
* PLAYWRIGHT_BASE_URL=http://localhost:8083 pour taper sur le build Nginx
|
||||||
|
* (au plus pres de la prod, utile en CI).
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
|
||||||
|
// Interdit `test.only` en CI pour ne pas skipper involontairement la suite.
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
|
||||||
|
// Pas de retry en local (bugs a reproduire), 2 retries en CI (flaky mitige).
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
|
||||||
|
// Parallelisme : 1 worker local pour faciliter le debug, defaut en CI.
|
||||||
|
workers: process.env.CI ? undefined : 1,
|
||||||
|
|
||||||
|
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
|
||||||
|
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3004',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
112
frontend/tests/e2e/_fixtures/personas.ts
Normal file
112
frontend/tests/e2e/_fixtures/personas.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* Definition des 6 personas utilises dans les tests E2E.
|
||||||
|
*
|
||||||
|
* Source de verite unique partagee entre :
|
||||||
|
* - le seed backend (`bin/console app:seed-e2e`)
|
||||||
|
* - les tests Playwright (via `loginAs`)
|
||||||
|
*
|
||||||
|
* Regle : chaque persona cible une case precise de la matrice RBAC.
|
||||||
|
* Si tu ajoutes une permission au domaine, tu NE crees pas un nouveau
|
||||||
|
* persona par reflexe — tu ajustes un persona existant si possible.
|
||||||
|
* L'objectif est de garder ce set petit et comprehensible a 6 mois.
|
||||||
|
*
|
||||||
|
* IMPORTANT : ces personas sont recrees a chaque `app:seed-e2e`. Ne jamais
|
||||||
|
* reutiliser les users dev (admin/alice/bob) dans les tests : ils evoluent
|
||||||
|
* au gre des fixtures de demo et casseraient la suite E2E.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PersonaKey =
|
||||||
|
| 'super-admin'
|
||||||
|
| 'user-full'
|
||||||
|
| 'user-readonly'
|
||||||
|
| 'user-users-only'
|
||||||
|
| 'user-audit-only'
|
||||||
|
| 'user-nothing'
|
||||||
|
|
||||||
|
export interface Persona {
|
||||||
|
key: PersonaKey
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
isAdmin: boolean
|
||||||
|
// Permissions directes attribuees en dur (on bypasse les roles pour
|
||||||
|
// garder le seed simple et la correspondance test<->permission directe).
|
||||||
|
permissions: string[]
|
||||||
|
// Contenu attendu de la sidebar (admin links). Utilise par le test
|
||||||
|
// sidebar-visibility pour driver la matrice. Les valeurs correspondent
|
||||||
|
// aux slugs de route (`/admin/<slug>`), volontairement stables quand
|
||||||
|
// la copie/i18n change.
|
||||||
|
expectedAdminLinks: Array<'users' | 'roles' | 'sites' | 'audit-log'>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHARED_PASSWORD = 'e2e-secret'
|
||||||
|
|
||||||
|
export const personas: Record<PersonaKey, Persona> = {
|
||||||
|
'super-admin': {
|
||||||
|
key: 'super-admin',
|
||||||
|
username: 'e2e.super-admin',
|
||||||
|
password: SHARED_PASSWORD,
|
||||||
|
isAdmin: true,
|
||||||
|
permissions: [],
|
||||||
|
expectedAdminLinks: ['users', 'roles', 'sites', 'audit-log'],
|
||||||
|
},
|
||||||
|
'user-full': {
|
||||||
|
key: 'user-full',
|
||||||
|
username: 'e2e.user-full',
|
||||||
|
password: SHARED_PASSWORD,
|
||||||
|
isAdmin: false,
|
||||||
|
permissions: [
|
||||||
|
'core.users.view',
|
||||||
|
'core.users.manage',
|
||||||
|
'core.roles.view',
|
||||||
|
'core.roles.manage',
|
||||||
|
'core.audit_log.view',
|
||||||
|
'sites.view',
|
||||||
|
'sites.manage',
|
||||||
|
'sites.bypass_scope',
|
||||||
|
],
|
||||||
|
expectedAdminLinks: ['users', 'roles', 'sites', 'audit-log'],
|
||||||
|
},
|
||||||
|
'user-readonly': {
|
||||||
|
key: 'user-readonly',
|
||||||
|
username: 'e2e.user-readonly',
|
||||||
|
password: SHARED_PASSWORD,
|
||||||
|
isAdmin: false,
|
||||||
|
permissions: [
|
||||||
|
'core.users.view',
|
||||||
|
'core.roles.view',
|
||||||
|
'core.audit_log.view',
|
||||||
|
'sites.view',
|
||||||
|
],
|
||||||
|
expectedAdminLinks: ['users', 'roles', 'sites', 'audit-log'],
|
||||||
|
},
|
||||||
|
'user-users-only': {
|
||||||
|
key: 'user-users-only',
|
||||||
|
username: 'e2e.user-users-only',
|
||||||
|
password: SHARED_PASSWORD,
|
||||||
|
isAdmin: false,
|
||||||
|
permissions: ['core.users.view', 'core.users.manage'],
|
||||||
|
expectedAdminLinks: ['users'],
|
||||||
|
},
|
||||||
|
'user-audit-only': {
|
||||||
|
key: 'user-audit-only',
|
||||||
|
username: 'e2e.user-audit-only',
|
||||||
|
password: SHARED_PASSWORD,
|
||||||
|
isAdmin: false,
|
||||||
|
permissions: ['core.audit_log.view'],
|
||||||
|
expectedAdminLinks: ['audit-log'],
|
||||||
|
},
|
||||||
|
'user-nothing': {
|
||||||
|
key: 'user-nothing',
|
||||||
|
username: 'e2e.user-nothing',
|
||||||
|
password: SHARED_PASSWORD,
|
||||||
|
isAdmin: false,
|
||||||
|
permissions: [],
|
||||||
|
expectedAdminLinks: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPersona(key: PersonaKey): Persona {
|
||||||
|
return personas[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ALL_ADMIN_LINKS = ['users', 'roles', 'sites', 'audit-log'] as const
|
||||||
65
frontend/tests/e2e/auth/login.spec.ts
Normal file
65
frontend/tests/e2e/auth/login.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { LoginPage } from '../helpers/pages/LoginPage'
|
||||||
|
import { getPersona } from '../_fixtures/personas'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests du flow login/logout via l'UI.
|
||||||
|
*
|
||||||
|
* C'est le SEUL fichier qui traverse le formulaire pour de vrai. Les autres
|
||||||
|
* specs utilisent `loginAs()` qui pose directement le cookie BEARER via API,
|
||||||
|
* 10x plus rapide et decouple du form HTML.
|
||||||
|
*/
|
||||||
|
test.describe('Login', () => {
|
||||||
|
test('login valide pose le cookie BEARER et redirige vers /', async ({ page, context }) => {
|
||||||
|
const superAdmin = getPersona('super-admin')
|
||||||
|
const loginPage = new LoginPage(page)
|
||||||
|
|
||||||
|
await loginPage.goto()
|
||||||
|
await loginPage.fillAndSubmit(superAdmin.username, superAdmin.password)
|
||||||
|
|
||||||
|
// La redirection se fait apres un `navigateTo('/')` dans login.vue.
|
||||||
|
await page.waitForURL('/')
|
||||||
|
await expect(page).toHaveURL('/')
|
||||||
|
|
||||||
|
// Le cookie BEARER (HTTP-only) doit etre pose par Symfony.
|
||||||
|
const cookies = await context.cookies()
|
||||||
|
const bearer = cookies.find(c => c.name === 'BEARER')
|
||||||
|
expect(bearer, 'Le cookie BEARER doit etre pose apres un login valide').toBeDefined()
|
||||||
|
expect(bearer?.httpOnly).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('login invalide reste sur /login et n\'emet pas de cookie', async ({ page, context }) => {
|
||||||
|
const loginPage = new LoginPage(page)
|
||||||
|
|
||||||
|
await loginPage.goto()
|
||||||
|
await loginPage.fillAndSubmit('e2e.super-admin', 'wrong-password')
|
||||||
|
|
||||||
|
// On ne doit PAS etre redirige — le handleSubmit swallow la 401 via toast,
|
||||||
|
// le user reste sur /login pour corriger.
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await expect(page).toHaveURL(/\/login$/)
|
||||||
|
|
||||||
|
const cookies = await context.cookies()
|
||||||
|
const bearer = cookies.find(c => c.name === 'BEARER')
|
||||||
|
expect(bearer, 'Aucun cookie BEARER ne doit etre pose apres un login invalide').toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('logout efface le cookie et redirige vers /login', async ({ page, context }) => {
|
||||||
|
const superAdmin = getPersona('super-admin')
|
||||||
|
const loginPage = new LoginPage(page)
|
||||||
|
|
||||||
|
// 1. Login d'abord
|
||||||
|
await loginPage.goto()
|
||||||
|
await loginPage.fillAndSubmit(superAdmin.username, superAdmin.password)
|
||||||
|
await page.waitForURL('/')
|
||||||
|
|
||||||
|
// 2. Navigation vers /logout (il y a un lien "Deconnexion" dans la sidebar)
|
||||||
|
await page.goto('/logout')
|
||||||
|
await page.waitForURL(/\/login$/)
|
||||||
|
|
||||||
|
// 3. Le cookie BEARER doit avoir ete supprime par le firewall de logout
|
||||||
|
const cookies = await context.cookies()
|
||||||
|
const bearer = cookies.find(c => c.name === 'BEARER')
|
||||||
|
expect(bearer, 'Le cookie BEARER doit etre supprime apres logout').toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
45
frontend/tests/e2e/helpers/loginAs.ts
Normal file
45
frontend/tests/e2e/helpers/loginAs.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { BrowserContext, Page } from '@playwright/test'
|
||||||
|
import { type PersonaKey, getPersona } from '../_fixtures/personas'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login programmatique : pose le cookie BEARER via l'API sans passer par le
|
||||||
|
* formulaire de login.
|
||||||
|
*
|
||||||
|
* Utilise ce helper dans TOUS les tests qui ne testent pas le flow login
|
||||||
|
* lui-meme (sidebar visibility, route guards, etc.). Ca evite de payer 2s
|
||||||
|
* par test sur le form HTML et ca isole les tests : si le form login casse,
|
||||||
|
* seul `login.spec.ts` est rouge, pas toute la suite.
|
||||||
|
*
|
||||||
|
* Impl : on issue une requete POST /api/login_check avec les creds du persona.
|
||||||
|
* Nginx reecrit vers /login_check, Symfony pose le cookie BEARER sur le
|
||||||
|
* context du browser. Apres ca, n'importe quelle navigation est authentifiee.
|
||||||
|
*/
|
||||||
|
export async function loginAs(context: BrowserContext, persona: PersonaKey, baseURL?: string): Promise<void> {
|
||||||
|
const { username, password } = getPersona(persona)
|
||||||
|
const base = baseURL ?? 'http://localhost:3004'
|
||||||
|
|
||||||
|
const response = await context.request.post(`${base}/api/login_check`, {
|
||||||
|
data: { username, password },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok()) {
|
||||||
|
const body = await response.text()
|
||||||
|
throw new Error(
|
||||||
|
`loginAs(${persona}) a echoue : ${response.status()} ${body}. `
|
||||||
|
+ 'Verifier que le backend tourne et que `make seed-e2e` a ete lance.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper d'appoint quand on veut tester VIA l'UI (login.spec.ts uniquement).
|
||||||
|
* Passe par le formulaire rendu, clique sur le bouton. A ne PAS utiliser
|
||||||
|
* dans les autres tests — preferer `loginAs()`.
|
||||||
|
*/
|
||||||
|
export async function loginViaForm(page: Page, persona: PersonaKey): Promise<void> {
|
||||||
|
const { username, password } = getPersona(persona)
|
||||||
|
await page.goto('/login')
|
||||||
|
await page.getByLabel("Nom d'utilisateur").fill(username)
|
||||||
|
await page.getByLabel('Mot de passe').fill(password)
|
||||||
|
await page.getByRole('button', { name: 'Se connecter' }).click()
|
||||||
|
}
|
||||||
32
frontend/tests/e2e/helpers/pages/LoginPage.ts
Normal file
32
frontend/tests/e2e/helpers/pages/LoginPage.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page Object du formulaire de login (/login).
|
||||||
|
*
|
||||||
|
* Selecteurs : on s'appuie sur les labels/roles accessibles (stable vs les
|
||||||
|
* changements de CSS/Tailwind). Le jour ou on veut un selecteur plus dur,
|
||||||
|
* on ajoute des `data-testid` sur login.vue.
|
||||||
|
*/
|
||||||
|
export class LoginPage {
|
||||||
|
readonly page: Page
|
||||||
|
readonly usernameInput: Locator
|
||||||
|
readonly passwordInput: Locator
|
||||||
|
readonly submitButton: Locator
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page
|
||||||
|
this.usernameInput = page.getByLabel("Nom d'utilisateur")
|
||||||
|
this.passwordInput = page.getByLabel('Mot de passe')
|
||||||
|
this.submitButton = page.getByRole('button', { name: 'Se connecter' })
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(): Promise<void> {
|
||||||
|
await this.page.goto('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
async fillAndSubmit(username: string, password: string): Promise<void> {
|
||||||
|
await this.usernameInput.fill(username)
|
||||||
|
await this.passwordInput.fill(password)
|
||||||
|
await this.submitButton.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
33
frontend/tests/e2e/helpers/pages/SidebarComponent.ts
Normal file
33
frontend/tests/e2e/helpers/pages/SidebarComponent.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
export type AdminLinkSlug = 'users' | 'roles' | 'sites' | 'audit-log'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page Object de la sidebar (MalioSidebar), scope sur les items "admin".
|
||||||
|
*
|
||||||
|
* Strategie selecteur : `a[href=...]` plutot que le texte i18n. Le slug de
|
||||||
|
* route ne change pas quand on retraduit ou renomme une entree — c'est le
|
||||||
|
* selecteur le plus stable pour cette suite.
|
||||||
|
*
|
||||||
|
* Si un jour la sidebar change et les slugs bougent, on met a jour CE
|
||||||
|
* fichier uniquement ; les specs continuent de passer.
|
||||||
|
*/
|
||||||
|
export class SidebarComponent {
|
||||||
|
readonly page: Page
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
adminLink(slug: AdminLinkSlug): Locator {
|
||||||
|
return this.page.locator(`a[href="/admin/${slug}"]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountDashboardLink(): Locator {
|
||||||
|
return this.page.locator('a[href="/"]').first()
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutLink(): Locator {
|
||||||
|
return this.page.locator('a[href="/logout"]')
|
||||||
|
}
|
||||||
|
}
|
||||||
82
frontend/tests/e2e/permissions/sidebar-visibility.spec.ts
Normal file
82
frontend/tests/e2e/permissions/sidebar-visibility.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { loginAs } from '../helpers/loginAs'
|
||||||
|
import { SidebarComponent } from '../helpers/pages/SidebarComponent'
|
||||||
|
import { ALL_ADMIN_LINKS, type PersonaKey, getPersona, personas } from '../_fixtures/personas'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test strategique : la matrice persona <-> liens admin visibles.
|
||||||
|
*
|
||||||
|
* Valide que `SidebarProvider` (back) + `useSidebar` (front) filtrent bien
|
||||||
|
* les items admin selon les permissions RBAC de chaque user.
|
||||||
|
*
|
||||||
|
* Regle d'evolution : ajouter une permission ou un persona = 1 ligne a
|
||||||
|
* modifier dans `personas.ts` et cote back (`SeedE2ECommand`) + `sidebar.php`.
|
||||||
|
* Ce fichier ne bouge pas.
|
||||||
|
*/
|
||||||
|
test.describe('Sidebar visibility', () => {
|
||||||
|
const personaKeys: PersonaKey[] = [
|
||||||
|
'super-admin',
|
||||||
|
'user-full',
|
||||||
|
'user-readonly',
|
||||||
|
'user-users-only',
|
||||||
|
'user-audit-only',
|
||||||
|
'user-nothing',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const key of personaKeys) {
|
||||||
|
const persona = getPersona(key)
|
||||||
|
|
||||||
|
test(`${persona.key} ne voit que ses liens admin autorises`, async ({ page, context }) => {
|
||||||
|
await loginAs(context, persona.key)
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
// Attendre que la sidebar soit chargee (le middleware auth fetch /api/sidebar
|
||||||
|
// apres login). Les liens presents apparaissent alors ; les absents ne
|
||||||
|
// seront jamais attaches au DOM.
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
const sidebar = new SidebarComponent(page)
|
||||||
|
|
||||||
|
for (const link of ALL_ADMIN_LINKS) {
|
||||||
|
const locator = sidebar.adminLink(link)
|
||||||
|
const shouldBeVisible = persona.expectedAdminLinks.includes(link)
|
||||||
|
|
||||||
|
if (shouldBeVisible) {
|
||||||
|
await expect(
|
||||||
|
locator,
|
||||||
|
`${persona.key} doit voir le lien /admin/${link}`,
|
||||||
|
).toBeVisible()
|
||||||
|
} else {
|
||||||
|
await expect(
|
||||||
|
locator,
|
||||||
|
`${persona.key} ne doit PAS voir le lien /admin/${link}`,
|
||||||
|
).toHaveCount(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('user-nothing voit toujours le dashboard et le logout (section Mon compte sans permission)', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
}) => {
|
||||||
|
// La section "Mon compte" n'est gardee par aucune permission : tout user
|
||||||
|
// authentifie voit le dashboard et peut se deconnecter. Ce test protege
|
||||||
|
// contre une regression qui mettrait un gate RBAC par inadvertance
|
||||||
|
// dessus — ca bloquerait le logout de users sans permissions.
|
||||||
|
await loginAs(context, 'user-nothing')
|
||||||
|
await page.goto('/')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
const sidebar = new SidebarComponent(page)
|
||||||
|
await expect(sidebar.accountDashboardLink()).toBeVisible()
|
||||||
|
await expect(sidebar.logoutLink()).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('la liste des personas dans personas.ts couvre toutes les combinaisons admin attendues', () => {
|
||||||
|
// Test meta : si quelqu'un ajoute un persona dans personas.ts sans le
|
||||||
|
// seeder cote back (SeedE2ECommand), le test sidebar pour ce persona
|
||||||
|
// echouera (loginAs 401). Ce test rappelle la coherence attendue.
|
||||||
|
expect(Object.keys(personas)).toEqual(personaKeys)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,6 +7,10 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
environment: 'happy-dom',
|
environment: 'happy-dom',
|
||||||
globals: true,
|
globals: true,
|
||||||
|
// Exclure les tests E2E Playwright : meme extension .spec.ts mais
|
||||||
|
// runtime different (navigateur vrai vs happy-dom). Playwright les
|
||||||
|
// ramasse via son propre testDir declare dans playwright.config.ts.
|
||||||
|
exclude: ['**/node_modules/**', '**/dist/**', 'tests/e2e/**'],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
35
makefile
35
makefile
@@ -38,7 +38,7 @@ restart: env-init
|
|||||||
$(DOCKER_COMPOSE) down
|
$(DOCKER_COMPOSE) down
|
||||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
|
|
||||||
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS migration-migrate test-db-setup
|
install: copy-git-hook composer-install cache-clear node-use build-nuxtJS migration-migrate sync-permissions test-db-setup
|
||||||
|
|
||||||
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
# Supprime tout est réinstalle tout (Attention ça supprime la bdd aussi)
|
||||||
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
reset: delete_built_dir remove_orphans build-without-cache start wait install
|
||||||
@@ -50,6 +50,7 @@ composer-install:
|
|||||||
$(SYMFONY_CONSOLE) lexik:jwt:generate-keypair --skip-if-exists
|
$(SYMFONY_CONSOLE) lexik:jwt:generate-keypair --skip-if-exists
|
||||||
|
|
||||||
build-nuxtJS:
|
build-nuxtJS:
|
||||||
|
$(EXEC_PHP_ROOT) chown -R $(APP_USER):$(APP_USER) /var/www/html/frontend
|
||||||
$(EXEC_PHP) sh -lc "cd frontend && npm install && npm run build:dist"
|
$(EXEC_PHP) sh -lc "cd frontend && npm install && npm run build:dist"
|
||||||
|
|
||||||
dev-nuxt:
|
dev-nuxt:
|
||||||
@@ -65,6 +66,38 @@ nuxt-lint-fix:
|
|||||||
nuxt-test:
|
nuxt-test:
|
||||||
$(EXEC_PHP) sh -c "cd frontend && npm run test"
|
$(EXEC_PHP) sh -c "cd frontend && npm run test"
|
||||||
|
|
||||||
|
# Seed les 6 personas E2E (idempotent). A relancer des que le catalogue
|
||||||
|
# permissions bouge (sync-permissions) ou avant chaque run test-e2e.
|
||||||
|
seed-e2e:
|
||||||
|
$(SYMFONY_CONSOLE) app:seed-e2e
|
||||||
|
|
||||||
|
# Bootstrap one-time pour les tests E2E sur un nouveau poste :
|
||||||
|
# 1. Telecharge Chromium dans ~/.cache/ms-playwright
|
||||||
|
# 2. Installe les deps systeme (libnss3, libasound, libatk, etc.) via
|
||||||
|
# la liste officielle Playwright — demande sudo.
|
||||||
|
#
|
||||||
|
# Le `sudo env "PATH=$$PATH"` est necessaire car avec NVM, `sudo npx` ne
|
||||||
|
# trouve pas npx (le PATH de sudo est vide par defaut). On preserve
|
||||||
|
# explicitement le PATH courant pour que npx resolve.
|
||||||
|
#
|
||||||
|
# A relancer uniquement si tu upgrade @playwright/test (les deps peuvent
|
||||||
|
# bouger entre versions majeures).
|
||||||
|
install-e2e-deps:
|
||||||
|
cd frontend && npx playwright install chromium
|
||||||
|
cd frontend && sudo env "PATH=$$PATH" npx playwright install-deps chromium
|
||||||
|
|
||||||
|
# Lance les tests E2E Playwright sur l'host. Pre-requis :
|
||||||
|
# - `make install-e2e-deps` (une fois par poste)
|
||||||
|
# - `make start` (containers en vie)
|
||||||
|
# - `make dev-nuxt` dans un autre terminal (serve frontend sur :3004)
|
||||||
|
# - `make seed-e2e` (personas crees)
|
||||||
|
test-e2e:
|
||||||
|
cd frontend && npm run test:e2e
|
||||||
|
|
||||||
|
# UI interactive Playwright (debug facile)
|
||||||
|
test-e2e-ui:
|
||||||
|
cd frontend && npm run test:e2e:ui
|
||||||
|
|
||||||
delete_built_dir:
|
delete_built_dir:
|
||||||
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
|
||||||
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
|
$(DOCKER) exec -u root $(PHP_CONTAINER) rm -rf vendor/
|
||||||
|
|||||||
205
src/Module/Core/Infrastructure/Console/SeedE2ECommand.php
Normal file
205
src/Module/Core/Infrastructure/Console/SeedE2ECommand.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Module\Core\Infrastructure\Console;
|
||||||
|
|
||||||
|
use App\Module\Core\Domain\Entity\User;
|
||||||
|
use App\Module\Core\Domain\Repository\PermissionRepositoryInterface;
|
||||||
|
use App\Module\Core\Domain\Repository\RoleRepositoryInterface;
|
||||||
|
use App\Module\Core\Domain\Repository\UserRepositoryInterface;
|
||||||
|
use App\Module\Core\Domain\Security\SystemRoles;
|
||||||
|
use App\Module\Sites\Domain\Repository\SiteRepositoryInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed dedie aux tests E2E Playwright (frontend/tests/e2e).
|
||||||
|
*
|
||||||
|
* Cree 6 personas (e2e.*) qui couvrent les cases nominales de la matrice
|
||||||
|
* RBAC : super-admin, user-full, user-readonly, user-users-only,
|
||||||
|
* user-audit-only, user-nothing. Cette liste est la replique back du
|
||||||
|
* fichier `frontend/tests/e2e/_fixtures/personas.ts` — si tu modifies
|
||||||
|
* l'une des deux, met a jour l'autre.
|
||||||
|
*
|
||||||
|
* Idempotent : supprime les users prefixes `e2e.` avant de les recreer.
|
||||||
|
* Ne touche PAS aux fixtures dev (admin/alice/bob) ni aux sites.
|
||||||
|
*
|
||||||
|
* Pre-requis : `bin/console app:sync-permissions` doit avoir tourne pour
|
||||||
|
* que les permissions soient en base. La commande echoue en erreur explicite
|
||||||
|
* si une permission attendue est absente du catalogue.
|
||||||
|
*/
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:seed-e2e',
|
||||||
|
description: 'Seed les 6 personas utilises par les tests E2E Playwright.',
|
||||||
|
)]
|
||||||
|
final class SeedE2ECommand extends Command
|
||||||
|
{
|
||||||
|
private const string SHARED_PASSWORD = 'e2e-secret';
|
||||||
|
private const string E2E_USERNAME_PREFIX = 'e2e.';
|
||||||
|
private const string DEFAULT_SITE_NAME = 'Chatellerault';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly UserRepositoryInterface $userRepository,
|
||||||
|
private readonly RoleRepositoryInterface $roleRepository,
|
||||||
|
private readonly PermissionRepositoryInterface $permissionRepository,
|
||||||
|
private readonly SiteRepositoryInterface $siteRepository,
|
||||||
|
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$userRole = $this->roleRepository->findByCode(SystemRoles::USER_CODE);
|
||||||
|
|
||||||
|
if (null === $userRole) {
|
||||||
|
$io->error(sprintf(
|
||||||
|
'Le role systeme "%s" est introuvable. Lance les migrations + fixtures ou `app:sync-permissions` avant ce seed.',
|
||||||
|
SystemRoles::USER_CODE,
|
||||||
|
));
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultSite = $this->siteRepository->findByName(self::DEFAULT_SITE_NAME);
|
||||||
|
|
||||||
|
// Pas de fail fatal si le site manque : les tests sidebar/login
|
||||||
|
// n'en dependent pas. Les tests sites-scope-bypass (a venir) le feront.
|
||||||
|
if (null === $defaultSite) {
|
||||||
|
$io->note(sprintf(
|
||||||
|
'Site "%s" absent : les personas seront crees sans site. Lance `make fixtures` si tu as besoin des sites.',
|
||||||
|
self::DEFAULT_SITE_NAME,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->wipeExistingE2EUsers($io);
|
||||||
|
|
||||||
|
foreach ($this->personasDefinition() as $persona) {
|
||||||
|
$user = new User();
|
||||||
|
$user->setUsername($persona['username']);
|
||||||
|
$user->setPassword($this->passwordHasher->hashPassword($user, self::SHARED_PASSWORD));
|
||||||
|
$user->setIsAdmin($persona['isAdmin']);
|
||||||
|
$user->addRbacRole($userRole);
|
||||||
|
|
||||||
|
foreach ($persona['permissions'] as $code) {
|
||||||
|
$permission = $this->permissionRepository->findByCode($code);
|
||||||
|
|
||||||
|
if (null === $permission) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Permission "%s" introuvable en base. Lance `app:sync-permissions` avant `app:seed-e2e`.',
|
||||||
|
$code,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->addDirectPermission($permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $defaultSite && 'e2e.user-nothing' !== $persona['username']) {
|
||||||
|
// user-nothing reste sans site pour pouvoir tester un flow
|
||||||
|
// "aucune permission et aucun site".
|
||||||
|
$user->addSite($defaultSite);
|
||||||
|
$user->setCurrentSite($defaultSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->userRepository->save($user);
|
||||||
|
|
||||||
|
$io->text(sprintf(
|
||||||
|
' - %s (admin=%s, permissions=%d)',
|
||||||
|
$persona['username'],
|
||||||
|
$persona['isAdmin'] ? 'oui' : 'non',
|
||||||
|
count($persona['permissions']),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->success(sprintf('%d personas E2E seedes.', count($this->personasDefinition())));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function wipeExistingE2EUsers(SymfonyStyle $io): void
|
||||||
|
{
|
||||||
|
$removed = 0;
|
||||||
|
|
||||||
|
foreach ($this->personasDefinition() as $persona) {
|
||||||
|
$existing = $this->userRepository->findByUsername($persona['username']);
|
||||||
|
|
||||||
|
if (null === $existing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->remove($existing);
|
||||||
|
++$removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($removed > 0) {
|
||||||
|
$this->em->flush();
|
||||||
|
$io->text(sprintf('Nettoyage : %d users E2E supprimes.', $removed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste des personas — source back, miroir de
|
||||||
|
* `frontend/tests/e2e/_fixtures/personas.ts`.
|
||||||
|
*
|
||||||
|
* @return list<array{username: string, isAdmin: bool, permissions: list<string>}>
|
||||||
|
*/
|
||||||
|
private function personasDefinition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'username' => self::E2E_USERNAME_PREFIX.'super-admin',
|
||||||
|
'isAdmin' => true,
|
||||||
|
'permissions' => [],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'username' => self::E2E_USERNAME_PREFIX.'user-full',
|
||||||
|
'isAdmin' => false,
|
||||||
|
'permissions' => [
|
||||||
|
'core.users.view',
|
||||||
|
'core.users.manage',
|
||||||
|
'core.roles.view',
|
||||||
|
'core.roles.manage',
|
||||||
|
'core.audit_log.view',
|
||||||
|
'sites.view',
|
||||||
|
'sites.manage',
|
||||||
|
'sites.bypass_scope',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'username' => self::E2E_USERNAME_PREFIX.'user-readonly',
|
||||||
|
'isAdmin' => false,
|
||||||
|
'permissions' => [
|
||||||
|
'core.users.view',
|
||||||
|
'core.roles.view',
|
||||||
|
'core.audit_log.view',
|
||||||
|
'sites.view',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'username' => self::E2E_USERNAME_PREFIX.'user-users-only',
|
||||||
|
'isAdmin' => false,
|
||||||
|
'permissions' => ['core.users.view', 'core.users.manage'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'username' => self::E2E_USERNAME_PREFIX.'user-audit-only',
|
||||||
|
'isAdmin' => false,
|
||||||
|
'permissions' => ['core.audit_log.view'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'username' => self::E2E_USERNAME_PREFIX.'user-nothing',
|
||||||
|
'isAdmin' => false,
|
||||||
|
'permissions' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user