Files
Metamorph/tasks/testing-m6.md
Knacky 00b7557e30 feat(m6): missions + snapshot CRUD, membership visibility, status state machine
Adds the mission layer that materialises template snapshots, plus the SPA
list / 3-step wizard / detail page.

Backend:
- app/services/missions.py — create_mission snapshots scenarios, tests, MITRE
  tags in a 4-query write; list/get apply a non-admin membership filter that
  collapses to 404 (no existence leak); status state machine enforces
  draft → in_progress → completed → archived with archived as a sink; the
  non-admin creator is auto-added as role_hint='red' to retain visibility.
- app/api/missions.py — 8 endpoints (list, get, create, update, add
  scenarios, set members, transition, soft-delete) with strict pydantic
  schemas. The transition endpoint splits the perm gate manually so
  archive requires mission.archive while other targets use mission.update.
- app/api/users.py — new GET /users/roster returning (id, email,
  display_name) only, gated by user.read OR mission.create OR
  mission.update — lets non-admin wizard users see assignable peers
  without exposing the admin /users payload.
- app/api/diag.py — /diag/reset truncates the mission_* tables before the
  template tables because the source_*_template_id FKs are ON DELETE SET
  NULL, which is cheaper to short-circuit by removing the children first.

Frontend:
- lib/missions.ts — typed client, queryKey factory, status accent map.
- pages/MissionsListPage.tsx — list cards with status accent + filters
  (q, client, status).
- pages/MissionsCreatePage.tsx — 3-step wizard (meta → scenarios → members)
  with member roster fed by /users/roster.
- pages/MissionDetailPage.tsx — header + transition buttons (legal next
  states only) + Tests/Members/Synthesis/Export tabs.
- Routes + nav entry (visible to anyone with mission.read or admin).

Tests:
- backend/tests/test_missions.py — 22 pytest covering snapshot fidelity,
  MITRE propagation, membership visibility, transition state machine,
  perm gating, member set replace, append scenarios, soft-delete, partial
  update, inverted-date rejection.
- e2e/tests/m6-missions.spec.ts — 5 Playwright (snapshot freezing, non-admin
  visibility, status transitions + 409, SPA wizard end-to-end, list filter).

Docs:
- CHANGELOG, tasks/testing-m6.md, tasks/lessons.md (snapshot tradeoffs,
  membership=404 pattern, /diag/reset order, auto-creator add).
- README + tasks/todo.md updated.

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

5.3 KiB

type, milestone, date, project
type milestone date project
testing M6 2026-05-13 Metamorph

Testing M6 — Missions & snapshot

1. Lancement de la stack

make clean
make up
make migrate
make seed-mitre  # MITRE tags are snapshotted onto mission_tests; without them
                 # the snapshot will simply have an empty mitre_tags array

L'admin stable admin@metamorph.local / AdminPass1234! est restauré automatiquement par le hook afterAll du spec e2e M6, mais la 1ʳᵉ fois, bootstrappe-le via /setup (ou laisse les tests faire le travail).

2. Tests automatisés

make test-api    # 103 tests pytest dont 22 M6 (snapshot, membership, transitions, members CRUD, perm gating)
make e2e         # 43 tests Playwright dont 5 M6 (snapshot freezing, non-admin visibility, transitions, wizard, list filter)

Rapport HTML : e2e/playwright-report/.

3. Smoke navigateur

Pré-requis

  • Stack make up + admin loggé.
  • MITRE seedé (/mitre montre 15 tactics).
  • Au moins 1 test_template et 1 scenario_template dans le catalogue M5 (pour avoir quelque chose à snapshotter).

3.1 Liste & création (/missions)

  1. Cliquer Missions dans la nav (visible si tu as la perm mission.read ou tu es admin) → la liste s'affiche avec un message vide la 1ʳᵉ fois.
  2. Cliquer + New mission → page wizard /missions/new.
  3. Étape 1 — Metadata :
    • Name (requis) → purple-2026-Q2
    • Client / targetAcme Corp
    • Start date / End date → si tu inverses, un message en rouge apparaît et Next est désactivé.
    • ROE / Description (markdown) → optionnel.
  4. NextÉtape 2 — Scenarios :
    • Le catalogue M5 s'affiche en grille de boutons. Cliquer un scénario le sélectionne (bordure cyan).
    • Le sous-titre du Card affiche le total de tests qui seront snapshotés.
  5. NextÉtape 3 — Members :
    • Le roster (issu de /users/roster) liste les utilisateurs actifs.
    • Pour chaque user, deux boutons Red / Blue togglent l'inclusion + le rôle. ✕ retire.
    • Si tu es un redteamer non-admin, tu es pré-sélectionné en red (auto-add côté backend si tu oublies).
  6. Create mission → redirection vers /missions/<id>. La nouvelle mission apparaît en haut de la liste après retour.

3.2 Filtres (/missions)

  • Search : full-text sur name / description_md.
  • Client : LIKE sur client_target.
  • Status : select draft / in_progress / completed / archived.
  • Les filtres sont combinés en AND (ex : status=in_progress & client=acme).

3.3 Page détail (/missions/<id>)

  1. En-tête : nom + status pill + boutons de transition.
    • draft → boutons → In Progress et → Archived.
    • in_progress→ Completed et → Archived.
    • completed→ Archived uniquement.
    • archived → aucun bouton.
  2. Cliquer un bouton → status update immédiat (cache invalidé, badge re-rendu).
  3. Delete (en rose) → confirm prompt → soft-delete → redirige vers /missions. Réapparait via ?include_deleted=true (admin only).
  4. Tabs :
    • tests : tableau par scénario avec # | Test | MITRE | OPSEC | State. Les MITRE chips affichent l'external_id frozen.
    • members : pills Red/Blue avec email + display_name.
    • synthesis : placeholder « lands in M10 ».
    • export : placeholder « lands in M11 ».

4. Vérification du snapshot (DoD)

  1. Crée une mission qui référence un scenario_template sc1 contenant test_template_t1.
  2. Aller dans /admin/tests, éditer test_template_t1 : changer le nom et les tags MITRE.
  3. Retour sur /missions/<id> (rafraîchir si la cache TanStack tient encore) → la table montre toujours l'ancien nom et l'ancien tag MITRE. Le snapshot est gelé.

5. Vérification visibilité par membership

  1. Login en admin, créer 2 missions :
    • m-only-admin sans aucun membre.
    • m-shared avec Alice (red) en membre.
  2. Login en Alice.
  3. /missions → seule m-shared apparaît dans la liste. GET /api/v1/missions/<m-only-admin> retourne 404 (pas 403 — pas de fuite d'existence).
  4. Alice tente de PUT/transition/delete sur m-only-admin → 404 idem.

6. Vérification transitions

from to result
draft in_progress 200
draft archived 200
draft completed 409 invalid_transition
in_progress completed 200
in_progress archived 200
completed archived 200
completed in_progress 409
archived (anything) 409
any (same status) 200 (no-op)
curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
     -d '{"status":"completed"}' \
     http://localhost:8080/api/v1/missions/<id>/transition

7. Quick teardown

make down
# ou pour un reset complet :
curl -X POST http://localhost:8080/api/v1/diag/reset  # test-only, wipes everything

Reminder: make test-api and make e2e share the dev DB container — running them mid-session WILL wipe user data. The M6 spec's afterAll restores the stable admin and re-seeds MITRE, but custom templates / missions you've created by hand are lost. Cf. tasks/lessons.md (M5 lessons section).