- 4 Playwright tests: API CRUD round-trip, scenario reorder via PUT, SPA list + opsec filter, SPA scenario list rendering with ordered tests. - afterAll restores the stable admin (admin@metamorph.local) per the test_admin memory rule. - CHANGELOG M5 section + Fixed subsections for the LogRecord 'name' collision and the React `currentTarget` vs `target` quirk. - README status bumps to M0-M5. - tasks/lessons.md captures the new patterns (sentinel pattern for partial-update, FK ordering in /diag/reset, dnd-kit stable IDs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
5.8 KiB
Markdown
132 lines
5.8 KiB
Markdown
---
|
|
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/<scn_id>/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.
|