feat(m0): bootstrap repo, design system, compose stack
- Repo scaffolding: .gitignore, .env.example, Makefile, docker-compose.yml, README.md, CHANGELOG.md, pre-commit config. - Three-service stack: api (Flask 3), db (postgres:16-alpine), front (nginx serving the Vite bundle). Named volumes metamorph_db + metamorph_evidence. - Backend skeleton: Flask app factory, JSON structured logging on stdout, GET /api/v1/health, multi-stage Dockerfile, pyproject.toml driven by uv, Pydantic Settings with secret guard rails (refuses to boot in non-dev with placeholders), APP_ENV gating. - Frontend skeleton: Vite + React 18 + TypeScript strict + TailwindCSS, RTOps design tokens from tasks/design.md, self-hosted JetBrains Mono / IBM Plex Sans via @fontsource, base UI primitives (Card/Tag/SectionHeader/FlowNode/ Button), home page wired to /api/v1/health. - Engine-agnostic Makefile: auto-detects docker or podman, picks the matching compose driver. Targets: up/down/build/rebuild/dev/lint/fmt/test/migrate/ seed-mitre/print-install-token/e2e/inspect-health. - Playwright suite: e2e/tests/m0-smoke.spec.ts (8 tests) + HTML + JUnit reports + traces on retry. - Docs: tasks/spec.md (finalized after Q&A), tasks/design.md, tasks/todo.md (14 milestones), tasks/testing-m0.md, tasks/lessons.md. DoD: make up + make health + make e2e all pass on podman 5.x (Fedora) and docker. TLS terminated by external reverse proxy (spec §6 NF-network). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
247
tasks/testing-m0.md
Normal file
247
tasks/testing-m0.md
Normal file
@@ -0,0 +1,247 @@
|
||||
---
|
||||
type: testing
|
||||
project: Metamorph
|
||||
milestone: M0
|
||||
date: "2026-05-10"
|
||||
---
|
||||
|
||||
# Comment tester M0 (bootstrap)
|
||||
|
||||
> Procédure de validation manuelle + automatisée pour le milestone M0. Toutes les commandes se lancent depuis la racine du repo.
|
||||
|
||||
## 0. Prérequis
|
||||
|
||||
Au choix entre Docker **ou** Podman — le Makefile détecte automatiquement (override `ENGINE=docker` ou `ENGINE=podman` si les deux sont installés).
|
||||
|
||||
| Outil | Version min | Vérifier |
|
||||
|--------------------------------|-------------------|-------------------------------------------|
|
||||
| **Docker Engine** *(option A)* | 24+ | `docker --version` + `docker compose version` |
|
||||
| **Podman** *(option B)* | 4.0+ avec plugin compose, ou podman-compose 1.0.6+ | `podman --version` + `podman compose version` (ou `podman-compose --version`) |
|
||||
| GNU make | 4+ | `make --version` |
|
||||
| curl, jq | n'importe | `curl --version` |
|
||||
| Node.js (pour les e2e) | 20+ | `node --version` |
|
||||
|
||||
Vérifier le moteur que le Makefile utilisera :
|
||||
```bash
|
||||
make engine
|
||||
# ENGINE=podman
|
||||
# COMPOSE=podman compose
|
||||
```
|
||||
|
||||
Override possible : `make up ENGINE=docker COMPOSE="docker compose"`.
|
||||
|
||||
## 1. Bootstrap de l'environnement
|
||||
|
||||
```bash
|
||||
make env # crée .env depuis .env.example
|
||||
$EDITOR .env # vérifie : APP_ENV=dev OK pour la machine, sinon set des secrets forts
|
||||
```
|
||||
|
||||
Variables critiques de `.env` :
|
||||
|
||||
- `APP_ENV` — `dev` autorise les placeholders ; `prod` ou `staging` exigent `JWT_SECRET >=32 chars` + `POSTGRES_PASSWORD` non-default (sinon l'API refuse de booter).
|
||||
- `JWT_SECRET` — pour M0 le démon ne signe rien, mais autant le mettre propre tout de suite : `python3 -c "import secrets; print(secrets.token_urlsafe(64))"`.
|
||||
- `HOST_FRONT_PORT` / `HOST_API_PORT` — modifie si 8080/8000 sont déjà occupés.
|
||||
|
||||
## 2. Build & démarrage
|
||||
|
||||
```bash
|
||||
make up # build des 3 images + démarrage
|
||||
make ps # vérifie que les 3 services tournent
|
||||
```
|
||||
|
||||
**Attendu** :
|
||||
```
|
||||
metamorph-db postgres:16-alpine Up (healthy)
|
||||
metamorph-api metamorph-api Up
|
||||
metamorph-front metamorph-front Up (healthy)
|
||||
```
|
||||
|
||||
Si `db` reste en `starting` au-delà de 30 s : `make logs` pour voir l'erreur (généralement un mismatch de credentials dans `.env`).
|
||||
|
||||
## 3. Tests fonctionnels manuels
|
||||
|
||||
### 3.1 — Health API direct (port 8000)
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8000/api/v1/health | jq
|
||||
```
|
||||
|
||||
**Attendu** :
|
||||
```json
|
||||
{ "status": "ok", "version": "0.1.0" }
|
||||
```
|
||||
|
||||
### 3.2 — Health API via le proxy nginx (port 8080)
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/api/v1/health | jq
|
||||
```
|
||||
|
||||
Doit renvoyer le **même** JSON. Cela valide la conf nginx qui proxifie `/api/* → api:8000`.
|
||||
|
||||
### 3.3 — SPA dans le navigateur
|
||||
|
||||
Ouvrir <http://localhost:8080>. **Vérifications visuelles** :
|
||||
|
||||
- [ ] Header centré, fond `#0a0e1a`, titre « Metamorph Purple Team Platform » avec « Meta » en rouge et « Purple Team Platform » en violet.
|
||||
- [ ] Section `// System Health` avec une card bordée vert affichant `version 0.1.0` et `status: ok`.
|
||||
- [ ] Section `// Design Tokens` montrant les tags colorés (EVASION, C2, LATERAL…), la flow chain `recon → phish → c2 → lateral → impact`, et 3 boutons.
|
||||
- [ ] Footer en mono dim avec la mention M0 bootstrap.
|
||||
- [ ] Toutes les polices sont chargées (titres en JetBrains Mono, body en IBM Plex Sans). Onglet Network : **aucune** requête vers `fonts.googleapis.com` ou `fonts.gstatic.com`.
|
||||
- [ ] Console JS sans aucune erreur.
|
||||
|
||||
### 3.4 — Logs structurés JSON
|
||||
|
||||
```bash
|
||||
make logs-api # tail uniquement le container api (engine-agnostic)
|
||||
```
|
||||
|
||||
**Attendu** : chaque ligne est un objet JSON avec au minimum `ts`, `level`, `logger`, `message`. Exemple :
|
||||
```json
|
||||
{"ts":"2026-05-10 14:21:33,012","level":"INFO","logger":"metamorph.boot","message":"metamorph.api.boot","cors_origins":["http://localhost:8080"],"log_level":"INFO","evidence_dir":"/data/evidence"}
|
||||
```
|
||||
|
||||
Si tu vois du texte non-JSON, c'est gunicorn qui parle ; vérifier que l'app est bien chargée via `app.main:app` (le formatter doit s'appliquer).
|
||||
|
||||
### 3.5 — Healthchecks containers
|
||||
|
||||
```bash
|
||||
make inspect-health
|
||||
```
|
||||
|
||||
**Attendu** : `healthy` pour les trois containers (à 30 s près après le boot).
|
||||
```
|
||||
metamorph-db healthy
|
||||
metamorph-api healthy
|
||||
metamorph-front healthy
|
||||
```
|
||||
|
||||
### 3.6 — Garde APP_ENV (sécurité)
|
||||
|
||||
Test négatif : on prouve que l'API refuse de booter en non-dev avec un secret faible.
|
||||
|
||||
```bash
|
||||
make down
|
||||
APP_ENV=prod JWT_SECRET=trop-court $(make engine | sed -n 's/^COMPOSE=//p') up api 2>&1 | head -30
|
||||
# ou plus simplement, en explicitant ton moteur :
|
||||
# APP_ENV=prod JWT_SECRET=trop-court docker compose up api
|
||||
# APP_ENV=prod JWT_SECRET=trop-court podman compose up api
|
||||
```
|
||||
|
||||
**Attendu** : trace d'erreur Pydantic mentionnant *"JWT_SECRET is missing, default, or shorter than 32 chars"*. L'API doit s'arrêter, pas démarrer.
|
||||
|
||||
Reset :
|
||||
```bash
|
||||
make down && make up
|
||||
```
|
||||
|
||||
### 3.7 — CORS
|
||||
|
||||
```bash
|
||||
curl -is -H 'Origin: http://localhost:8080' http://localhost:8080/api/v1/health \
|
||||
| grep -i access-control-allow-origin
|
||||
```
|
||||
|
||||
**Attendu** : un header `Access-Control-Allow-Origin: http://localhost:8080`.
|
||||
|
||||
```bash
|
||||
curl -is -H 'Origin: http://evil.example' http://localhost:8080/api/v1/health \
|
||||
| grep -i access-control-allow-origin || echo "no CORS allow header (expected)"
|
||||
```
|
||||
|
||||
**Attendu** : pas de header (origine non-allowée).
|
||||
|
||||
### 3.8 — Volumes persistants
|
||||
|
||||
```bash
|
||||
make volumes
|
||||
```
|
||||
|
||||
**Attendu** : deux volumes nommés (le préfixe peut varier selon le moteur) :
|
||||
```
|
||||
metamorph_db
|
||||
metamorph_evidence
|
||||
```
|
||||
|
||||
Test de persistance basique : `make down && make up` ne doit pas effacer les volumes ; seul `make clean` le fait (destructeur, demande explicite).
|
||||
|
||||
## 4. Tests automatisés (Playwright)
|
||||
|
||||
```bash
|
||||
make e2e-install # à faire une seule fois (download chromium + deps OS)
|
||||
make up # si la stack n'est pas déjà up
|
||||
make e2e # lance la suite
|
||||
make e2e-report # ouvre le rapport HTML
|
||||
```
|
||||
|
||||
**Suite M0** (`e2e/tests/m0-smoke.spec.ts`) — 8 tests :
|
||||
|
||||
| # | Test | Couvre |
|
||||
|---|-----------------------------------------------------------|-------------------------------------------------|
|
||||
| 1 | home page loads and renders the RTOps header | Front + nginx + assets statiques |
|
||||
| 2 | API health card eventually shows OK | Front → API via proxy `/api/*` |
|
||||
| 3 | design system primitives render with the expected accents | Card / Tag / FlowNode / Button |
|
||||
| 4 | body uses self-hosted IBM Plex Sans, no Google Fonts | Spec §7 « pas de CDN runtime » |
|
||||
| 5 | background uses the RTOps deep navy token | Token `--bg = #0a0e1a` appliqué |
|
||||
| 6 | no JS console errors on first load | Pas de regression silencieuse côté SPA |
|
||||
| 7 | API health endpoint returns the expected JSON shape | Contrat API direct |
|
||||
| 8 | CORS headers are set when the SPA origin asks for them | flask-cors configuré sur `FRONT_ORIGIN` |
|
||||
|
||||
Le rapport HTML (`e2e/playwright-report/index.html`) inclut, pour chaque test : steps, screenshots sur échec, vidéo sur retry, trace Playwright (timeline réseau + DOM).
|
||||
|
||||
Le rapport JUnit XML (`e2e/playwright-report/junit.xml`) est consommable directement par GitLab CI / GitHub Actions / Jenkins.
|
||||
|
||||
## 5. Tests unitaires backend
|
||||
|
||||
```bash
|
||||
make test-api
|
||||
```
|
||||
|
||||
**Attendu** : `tests/test_health.py::test_health_returns_ok PASSED`.
|
||||
|
||||
## 6. Lint & typecheck
|
||||
|
||||
```bash
|
||||
make lint
|
||||
```
|
||||
|
||||
Lance ruff (back), eslint + tsc --noEmit (front). Tout doit passer.
|
||||
|
||||
## 7. Critères de DoD M0 (extraits de `tasks/todo.md`)
|
||||
|
||||
- [ ] `make up` démarre les 3 conteneurs
|
||||
- [ ] `curl http://localhost:8080/api/v1/health` → `{"status":"ok","version":"…"}`
|
||||
- [ ] Front affiche la home RTOps (manuel + e2e #1, #3, #5)
|
||||
- [ ] Logs JSON sur stdout (manuel #3.4)
|
||||
- [ ] Volumes nommés présents (manuel #3.8)
|
||||
- [ ] Suite Playwright M0 verte
|
||||
- [ ] Rapport HTML disponible dans `e2e/playwright-report/`
|
||||
|
||||
## 8. Si quelque chose casse
|
||||
|
||||
| Symptôme | Diagnostic |
|
||||
|-------------------------------------------------------|---------------------------------------------------------|
|
||||
| `make up` plante en build du back | Probablement un download `uv` lent ; relancer ou `make rebuild` |
|
||||
| API réponse 502 via le front | api pas encore healthy ; `make logs api` |
|
||||
| Page blanche, console : `Failed to load module …` | Le bundle Vite n'a pas été produit ; `make rebuild` |
|
||||
| Polices custom non chargées (fallback sans-serif visible) | Vérifier que `@fontsource/*` est bien dans `node_modules` du build context |
|
||||
| Tests Playwright `Timeout … API health card` | API pas joignable depuis le navigateur ; tester `curl` d'abord |
|
||||
| `make volumes` ne montre rien | Vérifier que la stack est `make up`. Sous Podman rootless, les volumes vivent dans `~/.local/share/containers/storage/volumes/`. |
|
||||
| `make engine` annonce le mauvais moteur | Override : `make up ENGINE=docker COMPOSE="docker compose"` ou inverse pour podman. |
|
||||
| `podman compose` indisponible mais `podman-compose` oui | Le Makefile fallback automatiquement, ou force-le : `COMPOSE=podman-compose make up`. |
|
||||
|
||||
## 9. Pièges connus (validés sur podman 5.x / Fedora 43)
|
||||
|
||||
- **Short-name resolution sous Podman** : si tu remplaces une image par son nom court (`postgres:16-alpine`), Podman échoue avec `short-name resolution enforced but cannot prompt without a TTY`. **Toujours utiliser `docker.io/library/<image>:<tag>`** (Docker accepte le préfixe transparente).
|
||||
- **Premier `make up`** : compte ~3 min pour télécharger `postgres:16-alpine` + builder les images custom. Les builds suivants sont quasi instantanés grâce au cache.
|
||||
- **`make inspect-health` montre `(no-healthcheck)` malgré le Dockerfile** : podman-compose 1.x ne propage pas les healthchecks du Dockerfile. Le projet redéclare les healthchecks dans `docker-compose.yml` pour cette raison.
|
||||
- **`api` reste en `starting` ~15 s** avant de basculer healthy : c'est le `start_period: 10s` du healthcheck + 1 round de polling. Normal.
|
||||
- **Volumes Podman rootless** : `~/.local/share/containers/storage/volumes/` au lieu de `/var/lib/docker/volumes/`. `make volumes` liste les bons volumes peu importe l'engine.
|
||||
|
||||
## 10. Teardown
|
||||
|
||||
```bash
|
||||
make down # garde les volumes
|
||||
make clean # supprime aussi les volumes (DESTRUCTEUR)
|
||||
```
|
||||
Reference in New Issue
Block a user