User feedback flagged that the doc didn't reflect the two hotfixes shipped after the M7 PR: - evidence whitelist surfaced in the dropzone + OS picker pre-filter - executed_at override fixed in non-UTC timezones (no more time-snap) Added a CHANGELOG entry per fix and a §3.5 in tasks/testing-m7.md walking through the timezone semantics of the datetime-local input. spec.md is left untouched — these are UX/implementation fixes, not contract changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
208 lines
8.9 KiB
Markdown
208 lines
8.9 KiB
Markdown
---
|
|
type: testing
|
|
milestone: M7
|
|
date: "2026-05-14"
|
|
project: Metamorph
|
|
---
|
|
|
|
# Testing M7 — Red & blue execution on a mission test
|
|
|
|
## 1. Lancement de la stack
|
|
|
|
```bash
|
|
make up
|
|
make migrate # applies the M7 last_actor_id migration (91a4e7c6d2f3)
|
|
```
|
|
|
|
Le boot seede automatiquement les 4 detection_levels par défaut
|
|
(`detected_blocked` / `detected_alert` / `logged_only` / `not_detected`) via
|
|
`seed_detection_levels()`. Si tu pars d'un stack pré-existant, un `make
|
|
restart` (down+up) suffit — le seed est idempotent.
|
|
|
|
> L'admin stable `admin@metamorph.local / AdminPass1234!` est restauré par
|
|
> le hook `afterAll` du spec e2e M7. La 1ʳᵉ fois, bootstrappe-le via `/setup`.
|
|
|
|
## 2. Tests automatisés
|
|
|
|
```bash
|
|
make test-api # 131 tests pytest, dont 25 M7 (perm gating, state machine, evidence, activity)
|
|
make e2e # 48 tests Playwright, dont 5 M7 (red/blue gating, 24/26 MB, SHA256, SPA)
|
|
```
|
|
|
|
Rapport HTML : `e2e/playwright-report/`.
|
|
|
|
> **Reminder** : `make test-api` et `make e2e` partagent le Postgres dev.
|
|
> Lancer en milieu de session **wipe** les données — l'`afterAll` re-bootstrap
|
|
> l'admin stable, mais les missions/tests/uploads sur le disque créés à la
|
|
> main sont perdus.
|
|
|
|
## 3. Smoke navigateur
|
|
|
|
### Pré-requis
|
|
- Stack `make up` + admin loggé.
|
|
- Une mission existante avec au moins **1 scenario** snapshotté contenant
|
|
**≥ 1 test** (voir `testing-m6.md` pour le chemin de création).
|
|
|
|
### 3.1 Page de test (`/missions/<id>/tests/<test_id>`)
|
|
|
|
1. Depuis `/missions/<id>`, onglet **tests**, cliquer une ligne (ou le nom du
|
|
test). Redirection vers la page dédiée.
|
|
2. **En-tête** :
|
|
- `← Back to mission` (link `data-testid="back-to-mission"`).
|
|
- Nom du test (snapshot).
|
|
- Ligne *"Last touched Xs ago by Y"* — vide à la création, remplie dès qu'un
|
|
champ est sauvé.
|
|
- Status pill (`Pending` / `Executed` / `Reviewed` / `Skipped` / `Blocked`).
|
|
- Boutons de transitions autorisés depuis l'état courant (voir matrice en
|
|
§6).
|
|
3. **Card metadata** : MITRE chips, OPSEC tag, et 4 `<details>` pliés
|
|
(Objective / Procedure / Expected red / Expected blue).
|
|
|
|
### 3.2 Zone Red (bordure rouge)
|
|
- `Command` (mono, `data-testid="red-command"`).
|
|
- `Output` (textarea mono multilign, `data-testid="red-output"`).
|
|
- `Comment` (markdown, `data-testid="red-comment"`).
|
|
- Toggle **Override executed-at** + input datetime-local — disabled tant que
|
|
le test n'est pas `executed` / `reviewed_by_blue`.
|
|
- Bouton **Save red fields** :
|
|
- disabled si rien n'a changé ou si l'utilisateur n'a pas
|
|
`mission.write_red_fields`.
|
|
- Sur succès, le bandeau "Last touched" se met à jour (cache invalidé).
|
|
|
|
### 3.3 Zone Blue (bordure cyan)
|
|
- Select `Detection level` (sourcé de `/detection-levels`).
|
|
- `Comment` (markdown, `data-testid="blue-comment"`).
|
|
- Bouton **Save blue fields** (analogue à la zone red).
|
|
- **Evidence dropzone** :
|
|
- Drag & drop ou bouton **Pick files** (multi-fichiers).
|
|
- Sous la dropzone, la liste des extensions acceptées est affichée :
|
|
`Accepted: .png · .jpg · .jpeg · .pdf · .txt · .log · .json · .csv · .evtx · .zip · max 25 MB / file`
|
|
(testid `evidence-allowed-formats`).
|
|
- Le sélecteur OS est pré-filtré via l'attribut `accept` — pas de "All files".
|
|
- Un drag&drop d'une extension hors-liste est rejeté côté client avec
|
|
le message d'erreur en rouge ; le serveur re-vérifie quoi qu'il
|
|
arrive (`unsupported_extension` / `too_large`).
|
|
- Limite : **25 MB / fichier** (côté client = garde-fou UX, côté serveur =
|
|
stricte avec stream cap chunk-par-chunk).
|
|
- Table récap : nom · taille · uploader · `sha256[:12]…` · link download +
|
|
bouton soft-delete.
|
|
|
|
### 3.5 Override executed-at — pièges timezone
|
|
- Le toggle **Override executed-at timestamp** affiche un input
|
|
`datetime-local` qui parle en **heure locale du navigateur** (ex.
|
|
Europe/Paris UTC+2). L'état local du composant garde la valeur sous
|
|
forme `YYYY-MM-DDTHH:MM` ; la conversion en UTC ISO n'a lieu qu'au
|
|
submit. Donc si tu tapes `10:30` à Paris, le serveur reçoit
|
|
`08:30:00+00:00` — c'est attendu.
|
|
- Le polling activity (15 s) ne re-sync l'état local **que** sur changement
|
|
d'identité du test (`useEffect([test.id])`) — une frappe en cours n'est
|
|
jamais écrasée par un refetch.
|
|
|
|
### 3.4 Indicateur d'activité
|
|
|
|
- À l'arrivée sur la page, le polling `GET /missions/<id>/activity` démarre
|
|
(toutes les 15 s, gated sur `document.visibilityState === 'visible'`).
|
|
- Si un autre user édite le test, la query est invalidée → la page reload
|
|
les champs (TanStack cache replaced).
|
|
- Le `server_time` est passé en `?since=` à l'appel suivant pour ne recevoir
|
|
que ce qui a bougé depuis.
|
|
|
|
## 4. Vérifications fonctionnelles (DoD)
|
|
|
|
### 4.1 Red écrit en parallèle de Blue, sans conflit
|
|
|
|
1. Sur un test `pending`, login en red dans 1 onglet, en blue dans un autre.
|
|
2. Red : remplit `red_command` + sauve.
|
|
3. Blue : sélectionne `detected_alert` + commentaire + sauve.
|
|
4. Les 2 saves passent en 200, aucun conflit.
|
|
5. Rafraîchir l'onglet red → les champs blue apparaissent (et réciproquement).
|
|
|
|
### 4.2 Perm gating field-level
|
|
|
|
| User | red_command | red_comment | blue_comment | detection_level | upload |
|
|
|--------------|------------:|------------:|-------------:|----------------:|-------:|
|
|
| red | ✓ | ✓ | **403** | **403** | **403** |
|
|
| blue | **403** | **403** | ✓ | ✓ | ✓ |
|
|
| red + blue | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
| admin | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
|
|
### 4.3 Evidence upload — limites
|
|
|
|
1. Upload un fichier `.evtx` de **24 MB** → 201, body inclut `sha256`,
|
|
`size_bytes=25165824`, `mime=application/octet-stream`.
|
|
2. Vérif `sha256` côté client : `sha256sum file24.evtx` == `body.sha256`.
|
|
3. Upload un fichier `.evtx` de **26 MB** → 400 `{error:"too_large"}`.
|
|
4. Upload un fichier `.exe` (1 octet) → 400 `{error:"unsupported_extension"}`.
|
|
5. Download via le lien `download` → bytes byte-for-byte identiques.
|
|
|
|
### 4.4 Soft delete d'evidence
|
|
1. Upload un PDF, vérif qu'il apparaît dans la table.
|
|
2. Cliquer **delete** → confirmation → row disparaît.
|
|
3. `GET /evidence/<id>` → 404 (le row reste en DB avec `deleted_at` set,
|
|
mais le service l'occulte).
|
|
4. Sur disque, `/data/evidence/<mission_id>/<test_id>/<sha256>.pdf` est
|
|
**conservé** (purge physique = M12).
|
|
|
|
## 5. Vérification du state machine
|
|
|
|
| from | to | result | side requis |
|
|
|--------------------|-------------------|--------|-------------|
|
|
| pending | executed | 200 | red |
|
|
| pending | skipped | 200 | any |
|
|
| pending | blocked | 200 | any |
|
|
| pending | reviewed_by_blue | **409** | — |
|
|
| executed | reviewed_by_blue | 200 | blue |
|
|
| executed | pending | 200 | red (reset) |
|
|
| reviewed_by_blue | executed | 200 | blue |
|
|
| reviewed_by_blue | pending | **409** | — |
|
|
| skipped | pending | 200 | any |
|
|
| blocked | pending | 200 | any |
|
|
| any | (same state) | 200 | — (no-op) |
|
|
|
|
```bash
|
|
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
|
|
-d '{"target_state":"executed"}' \
|
|
http://localhost:8080/api/v1/missions/<mid>/tests/<tid>/transition
|
|
```
|
|
|
|
Side-effect attendu : `target_state="executed"` stamp `executed_at=now()` et
|
|
remet `executed_at_overridden=false`. Le retour à `pending` efface
|
|
`executed_at`.
|
|
|
|
## 6. Vérification override executed_at
|
|
|
|
1. État `pending` → PUT `{"executed_at": "...", "executed_at_overridden": true}`
|
|
→ **400** (refusé tant que le test n'a pas été marqué executed).
|
|
2. Transition `pending → executed` → `executed_at` auto-stamp.
|
|
3. PUT `{"executed_at":"2026-05-14T10:00:00+00:00","executed_at_overridden":true}`
|
|
→ 200, body reflète la nouvelle date + override=true.
|
|
4. Blue user tente le même PUT → **403** (executed_at est red-side).
|
|
|
|
## 7. Vérification activity polling
|
|
|
|
```bash
|
|
# Snapshot t0
|
|
curl -H "Authorization: Bearer $T" \
|
|
http://localhost:8080/api/v1/missions/<mid>/activity \
|
|
| jq .server_time
|
|
# Mutate
|
|
curl -X PUT -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
|
|
-d '{"red_comment_md":"poke"}' \
|
|
http://localhost:8080/api/v1/missions/<mid>/tests/<tid>
|
|
# Poll t1 (URL-encode the timestamp's `+`)
|
|
SINCE=$(python -c "import urllib.parse;print(urllib.parse.quote('${T0}'))")
|
|
curl -H "Authorization: Bearer $T" \
|
|
"http://localhost:8080/api/v1/missions/<mid>/activity?since=${SINCE}"
|
|
```
|
|
|
|
Réponse attendue : 1 entrée pour le test mis à jour, avec `last_actor_email`
|
|
peuplé.
|
|
|
|
## 8. Quick teardown
|
|
|
|
```bash
|
|
make down
|
|
# ou reset complet (test-only) :
|
|
curl -X POST http://localhost:8080/api/v1/diag/reset
|
|
```
|