Files
Metamorph/tasks/testing-m1.md

219 lines
8.3 KiB
Markdown
Raw Normal View History

2026-05-11 06:05:27 +02:00
---
type: testing
project: Metamorph
milestone: M1
date: "2026-05-10"
---
# Comment tester M1 (schéma DB & migrations)
> Procédure de validation manuelle + automatisée pour M1 : SQLAlchemy 2 + Alembic + 26 tables. Toutes les commandes se lancent depuis la racine du repo.
## 0. Prérequis
Voir `tasks/testing-m0.md §0` (Docker ou Podman, Make, Node 20+, etc.). Aucune dépendance Python locale n'est requise — pytest tourne dans un container éphémère bâti depuis le stage `test` du Dockerfile backend.
## 1. Bootstrap (si la stack n'est pas déjà up)
```bash
make env # crée .env si absent
make up # build + start de la stack (api / db / front)
make inspect-health # attends que les 3 soient healthy
```
## 2. Appliquer la migration
```bash
make migrate # alembic upgrade head dans le container api
make migrate-status # confirme la revision courante = head
```
**Attendu** :
```
INFO [alembic.runtime.migration] Will assume transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> 24765a5014b6, initial schema
```
et `make migrate-status` :
```
24765a5014b6 (head)
---
24765a5014b6 (head)
```
## 3. Tests fonctionnels manuels
### 3.1 — Liste des tables
```bash
make psql # ouvre psql dans le container db
\dt # une fois dans psql
```
**Attendu** : **27 lignes** (26 tables métier + `alembic_version`) :
```
detection_levels, evidence_files, group_permissions, groups,
invitation_groups, invitations, mission_categories, mission_members,
mission_scenarios, mission_test_mitre_tags, mission_tests, missions,
mitre_subtechniques, mitre_tactics, mitre_technique_tactics,
mitre_techniques, notifications, permissions, refresh_tokens,
scenario_template_tests, scenario_templates, settings,
test_template_mitre_tags, test_templates, user_groups, users,
alembic_version
```
Compter via SQL :
```bash
podman exec metamorph-db psql -U metamorph -d metamorph -tAc \
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'"
# 27
```
### 3.2 — Contraintes au niveau Postgres
```bash
podman exec metamorph-db psql -U metamorph -d metamorph -tAc \
"SELECT contype, count(*) FROM pg_constraint WHERE connamespace = 'public'::regnamespace GROUP BY contype ORDER BY contype"
```
**Attendu** :
- `c|9` — CHECK constraints (status valid, opsec_level valid, mitre_kind valid, exactly_one_mitre_fk uniquement sur `test_template_mitre_tags`, …)
- `f|32` — Foreign keys *(snapshot `mission_test_mitre_tags` n'a volontairement pas de FK MITRE)*
- `p|27` — Primary keys (1 par table)
- `u|14` — UNIQUE constraints
### 3.3 — Index partiels (soft delete + unread notifications)
```bash
podman exec metamorph-db psql -U metamorph -d metamorph -c \
"SELECT indexname FROM pg_indexes WHERE schemaname='public' AND indexdef ILIKE '%WHERE%' ORDER BY 1"
```
**Attendu** (12 indexes) :
- `ix_evidence_files_active`, `ix_groups_active`, `ix_missions_active`, `ix_mission_categories_active`, `ix_mission_scenarios_active`, `ix_mission_tests_active`, `ix_scenario_templates_active`, `ix_test_templates_active`, `ix_users_active` — soft-delete partiels (9)
- `ix_notifications_user_unread``WHERE read_at IS NULL`
- `uq_users_email_active`, `uq_groups_name_active` — uniques scopés aux lignes actives
### 3.4 — Test négatif d'un CHECK constraint (`exactly_one_mitre_fk`)
```sql
-- Dans psql, doit échouer :
INSERT INTO test_templates (id, name, opsec_level)
VALUES (gen_random_uuid(), 'tmp', 'low');
INSERT INTO mitre_tactics (id, external_id, short_name, name)
VALUES (gen_random_uuid(), 'TA0099', 'tmp', 'tmp');
-- (puis tente une insertion avec deux FK MITRE non null — bloqué par CHECK)
```
Couvert automatiquement par `tests/test_schema.py::test_exactly_one_mitre_fk_check_enforced`.
### 3.5 — Migration depuis DB totalement vide
```bash
make clean # DESTRUCTEUR — supprime aussi les volumes
make up # re-spawn db vierge
# attends que db soit healthy
make migrate # applique 0001_initial sur DB vide
podman exec metamorph-db psql -U metamorph -d metamorph -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema='public'"
# attendu : 27
```
## 4. Tests automatisés
### 4.1 — Backend pytest (M1 schema integration)
```bash
make test-api
```
**Attendu** : `9 passed in <1s` (1 health M0 + 8 schema M1).
Détail des 8 tests M1 :
| # | Test | Couvre |
|---|---|---|
| 1 | `test_all_expected_tables_exist` | Liste exhaustive des 26 tables métier |
| 2 | `test_soft_delete_columns_present` | `deleted_at` sur 6 tables |
| 3 | `test_standard_timestamp_columns_present` | `created_at`+`updated_at` sur 5 tables |
| 4 | `test_partial_index_for_soft_delete` | Index `ix_<table>_active` partiel |
| 5 | `test_expected_foreign_keys` | 14 paires FK clés (red→users, blue→users, evidence→test, etc.) |
| 6 | `test_expected_check_constraints` | Les 10 CHECK constraints fonctionnelles |
| 7 | `test_alembic_at_head` | `SELECT version_num FROM alembic_version` non-vide |
| 8 | `test_exactly_one_mitre_fk_check_enforced` | Test négatif INSERT — viole le CHECK |
Le test runner s'appuie sur le stage `test` du Dockerfile backend (`--target test` avec uv et les dev extras), spawné en container éphémère sur le réseau du compose. Le runtime stays minimal.
### 4.2 — Suite e2e Playwright (M0 + M1)
```bash
make e2e
```
**Attendu** : `12 passed`. Détail :
- 8 tests M0 (smoke bootstrap)
- 4 tests M1 (`e2e/tests/m1-db.spec.ts`) :
1. `GET /api/v1/diag/db` renvoie une revision Alembic en hex et `table_count >= 26`
2. La home page rend la card « Database » avec le short-hash de la revision et le compteur
3. La card « Roadmap » indique « M0 + M1 done » et cite M2
4. Le footer mentionne `M0 bootstrap` + `M1 db schema`
Le rapport HTML est dans `e2e/playwright-report/`.
### 4.3 — Endpoint diagnostique direct
```bash
curl -s http://localhost:8080/api/v1/diag/db | jq
```
**Attendu** :
```json
{
"reachable": true,
"alembic_revision": "24765a5014b6",
"table_count": 27
}
```
Quand la DB est down (ex : `make down` sur le service `db` seul), l'endpoint renvoie `503` avec `{"reachable": false, "error": "database_unreachable"}`.
## 5. Génération d'une nouvelle migration (workflow dev)
```bash
# 1. Modifier un modèle dans backend/app/models/
# 2. Générer la migration via Alembic dans le container :
make migrate-revision MSG="add foo column to mission_tests"
# Le fichier est créé dans le container — copie-le sur l'host pour le commit :
podman cp metamorph-api:/app/alembic/versions/. backend/alembic/versions/
# 3. Relire le fichier généré, le formatter (`make fmt`)
# 4. Rebuild + apply :
make rebuild && make up && make migrate
```
## 6. DoD M1 — checklist (extraits de `tasks/todo.md`)
- [x] `make migrate` applique le schéma sur DB vide
- [x] `\dt` montre les 27 tables (26 métier + alembic_version)
- [x] FK + CHECK + indexes en place (32 FK / 9 CHECK / 14 UQ / 12 partial)
- [x] Naming convention Alembic stable (préfixes `pk_/fk_/ck_/uq_/ix_`)
- [x] Soft delete partout sauf jointures simples (`deleted_at` + index partiel)
- [x] Audit minimal (`created_at`/`updated_at`) sur les tables principales
- [x] Tests d'intégration pytest verts (9 passed)
- [x] M0 e2e ne régresse pas
## 7. Pièges connus
- **`COMPOSE` cible le dernier stage du Dockerfile par défaut** : si on ajoute un stage après `runtime` (ici `test`), il faut explicitement `target: runtime` dans `docker-compose.yml`. Sinon `make up` lance pytest au lieu de gunicorn — le container exit en boucle.
- **Alembic autogenerate dans le container** : le fichier est créé dans `/app/alembic/versions/` du container. Le récupérer sur l'host via `podman cp` avant rebuild, sinon perdu.
- **Post-write hook `ruff`** : retiré d'`alembic.ini` parce que ruff est dev-only et n'est pas dans l'image runtime. Formatter les migrations à la main avec `make fmt` après génération.
- **`change-me-strong` (placeholder de `.env.example`)** est rejeté par `model_validator` en `APP_ENV=prod`. Pour les tests on a élargi le bypass à `APP_ENV in ("dev", "test")`.
## 8. Teardown
```bash
make down # garde les volumes
make clean # supprime aussi les volumes (DESTRUCTEUR)
```