--- type: testing milestone: M5 date: "2026-05-12" project: Metamorph --- # Testing M5 — Templates : tests unitaires & scénarios ## 1. Lancement de la stack ```bash make clean make up make migrate make seed-mitre # tag picker needs the catalogue ``` > L'admin stable `admin@metamorph.local / AdminPass1234!` est restauré > automatiquement par le hook `afterAll` du spec e2e M5 — mais la 1ʳᵉ fois, > bootstrappe-le via `/setup` ou laisse les tests faire le travail. ## 2. Tests automatisés ```bash make test-api # 77 tests pytest dont 19 M5 (CRUD, perm, mitre tags, reorder) make e2e # 38 tests Playwright dont 4 M5 (API CRUD + scenario reorder + SPA list/filter) ``` Rapport HTML : `e2e/playwright-report/`. JUnit : `e2e/playwright-report/junit.xml`. ## 3. Smoke navigateur ### Pré-requis - Stack `make up` + admin loggé. - MITRE seedé (vérifier via `/mitre`). ### 3.1 Catalogue de tests (`/admin/tests`) 1. Cliquer **Tests** dans la nav admin → page chargée. 2. Cliquer **+ New test** → modal s'ouvre avec : - Champs : Name, Description, Objective, Procedure (markdown), Prerequisites, Red expected, Blue expected, OPSEC, Free tags, Expected IOCs. - Sous-section **MITRE ATT&CK tags** : matrice complète, mêmes interactions que `/mitre`. 3. Remplir au minimum `Name=phish-link`, OPSEC=`low`, ajouter 2 tags MITRE (ex. `TA0001 + T1566`) → **Create** → carte apparaît dans la liste avec chips OPSEC + MITRE. 4. Cliquer **Edit** sur la carte → modal pré-remplie, modifier OPSEC à `high` → **Save** → la card est repeinte avec l'accent rouge OPSEC. 5. Filtres en haut : - `Search` (full-text q sur nom/description) - `Tactic external_id` (ex. `TA0001`) - `OPSEC` (select : —all— / low / medium / high) - `Free tag` (mot-clé libre) 6. Cliquer **Delete** sur une carte → confirm popup → la card disparaît (soft-delete : visible via `?include_deleted=true` côté API). ### 3.2 Catalogue de scénarios (`/admin/scenarios`) 1. Cliquer **Scenarios** dans la nav admin. 2. **+ New scenario** → modal. - Champs Name + Description. - Catalogue picker en bas : champ de recherche + liste des tests dispos (max 50). 3. Cliquer 3 tests dans le catalogue → ils s'empilent dans la liste ordonnée avec leurs indices `01/02/03`. 4. **Drag-and-drop** : empoigner la poignée `☰` à gauche d'une ligne et glisser vers le haut/bas → la liste se réordonne. La grille met à jour les indices au relâchement. 5. **Save** → carte apparaît avec un Tag « N tests » + l'aperçu des 4 premiers tests dans l'ordre choisi. 6. Re-ouvrir Edit → l'ordre est persisté côté serveur (vérifie le numéro 01, 02, 03 dans la modal). 7. Supprimer un `test_template` dont un scénario dépend (via `/admin/tests`) → la card scénario marque le test en rose dans le résumé (`test_template_deleted: true`). ### 3.3 Permissions 1. Inviter Bob via Admin > Invitations sans groupe → Bob peut se logger mais reçoit `403` sur `/api/v1/test-templates`. 2. Lui attacher un groupe avec seulement `test_template.read` → Bob voit `/admin/tests`... non, l'UI gate sur `is_admin`. La perm seule donne l'accès API ; l'UI ne l'expose pas pour les non-admins (par design M5). 3. Bob tente `POST /api/v1/test-templates` → `403` (manque `test_template.create`). ## 4. Smoke API ### 4.1 Login admin ```bash ACCESS=$(curl -sX POST http://localhost:8080/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"admin@metamorph.local","password":"AdminPass1234!"}' | jq -r .access_token) ``` ### 4.2 Créer un test taggué MITRE ```bash curl -sX POST http://localhost:8080/api/v1/test-templates \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d '{ "name": "lsass-dump", "opsec_level": "high", "tags": ["creds"], "mitre_tags": [ {"kind":"technique","external_id":"T1003"}, {"kind":"subtechnique","external_id":"T1003.001"} ] }' | jq ``` ### 4.3 Créer un scénario ordonné ```bash # Suppose 3 ids: $A $B $C curl -sX POST http://localhost:8080/api/v1/scenario-templates \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d "{\"name\":\"chained\",\"test_template_ids\":[\"$A\",\"$B\",\"$C\"]}" | jq # Reorder (full replace) curl -sX PUT http://localhost:8080/api/v1/scenario-templates//tests \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d "{\"test_template_ids\":[\"$C\",\"$A\",\"$B\"]}" | jq ``` ### 4.4 Filtre par tactic ```bash curl -s "http://localhost:8080/api/v1/test-templates?tactic=TA0006" \ -H "Authorization: Bearer $ACCESS" | jq '.items[].name' ``` ## 5. Points de contrôle critiques - [x] `POST /test-templates` rejette MITRE inconnu avec `400 unknown_mitre_tag`. - [x] `POST /test-templates` rejette opsec hors `low/medium/high`. - [x] `PUT /test-templates/{id}` partial keeps unset fields. - [x] `PUT /test-templates/{id}` avec `mitre_tags` **remplace** la collection (pas d'append). - [x] `DELETE /test-templates/{id}` soft-delete (visible avec `?include_deleted=true`). - [x] `POST /scenario-templates` rejette test_template inconnu ou soft-deleted. - [x] `PUT /scenario-templates/{id}/tests` rewrite atomique (delete + re-insert, contrainte UNIQUE(position) honorée). - [x] Un test soft-deleted **après** linking reste référencé : `test_template_deleted: true` sur le scénario. - [x] Filtres list: `q`, `tactic`, `technique`, `subtechnique`, `opsec`, `tag` cumulatifs. - [x] Perm gating : `test_template.{read,create,update,delete}` + `scenario_template.{read,create,update,delete}`. - [x] `/diag/reset` truncate les 4 nouvelles tables (`scenario_template_tests`, `scenario_templates`, `test_template_mitre_tags`, `test_templates`) avant les tables MITRE. - [x] UI : drag-and-drop @dnd-kit/sortable réordonne la liste, save persistant.