Files
Metamorph/tasks/testing-m5.md
Knacky ce4bd40551 fix(m5): post-review pass — AND filter, advisory lock, N+1, item caps, mutation cache
Spec-reviewer + code-reviewer findings applied:

Must-fix
- Filter combinator AND-semantics: tactic+technique+subtechnique now intersect
  (one IN subquery per facet) instead of being pooled into one OR. Reviewers
  flagged both the wrong default semantics and the theoretical UUID-collision
  risk of pooling tactic/technique/sub UUIDs into a shared list across
  three columns.
- Front-end mutation cache hygiene: updateMeta + setTests both
  `onSettled: invalidate` so a partial failure leaves the cache consistent.

Should-fix
- Per-scenario pg_advisory_xact_lock on set_scenario_tests — serialises
  concurrent reorders, mirrors M4 /mitre/sync pattern.
- Backend/front consistency on duplicate tests in a scenario: the
  UNIQUE(scenario_id, position) constraint already allows the same
  test_template multiple times (chained ops), so the catalogue picker no
  longer excludes already-picked items.

Nice-to-have
- N+1 eradicated in test_template view rendering: _to_views_batch
  builds {uuid → MitreRow} maps in 3 queries up-front; list endpoint
  now issues 4 queries total regardless of list size.
- Wire-level item length caps on tags (64) and expected_iocs (255)
  via Annotated[str, StringConstraints(...)] — returns 400 instead of
  bubbling up StringDataRightTruncation.
- 4 new pytest covering the AND-filter, extra="forbid" rejection,
  empty mitre_tags clearing, and the 65-char tag cap. Total now
  81 pytest + 38 e2e pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:05:00 +02:00

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 # 81 tests pytest dont 23 M5 (CRUD, perm, mitre tags, reorder, AND-semantics, extra="forbid", item caps, empty-clear)
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.