8.2 KiB
type, project, milestone, date
| type | project | milestone | date |
|---|---|---|---|
| testing | Metamorph | M2 | 2026-05-10 |
Comment tester M2 (auth, bootstrap, invitations)
Procédure de validation manuelle + automatisée pour M2 (auth JWT, /setup, invitations, RBAC). Toutes les commandes se lancent depuis la racine.
0. Prérequis
Voir tasks/testing-m0.md §0. M2 n'ajoute aucune dépendance host (le pytest tourne dans un container éphémère via make test-api).
1. Bootstrap stack vide → premier admin
make env
make up
make migrate # 27 tables (M1)
Récupère le token install dans les logs :
make logs-api | grep -E "INSTALL TOKEN" | tail -1
# ou : podman logs metamorph-api 2>&1 | grep "INSTALL TOKEN"
Si la stack a été utilisée et le token consommé, force-mint un nouveau :
make print-install-token-force
2. /setup — création du 1er admin via curl
TOKEN="<paste-token-here>"
curl -s -X POST http://localhost:8080/api/v1/setup \
-H "Content-Type: application/json" \
-d "{\"install_token\":\"$TOKEN\",\"email\":\"admin@metamorph.local\",\"password\":\"AdminPass1234!\"}" | jq
# Attendu : {"ok":true,"user_id":"..."}
Vérifie que /setup ne peut plus être consommé :
curl -s http://localhost:8080/api/v1/setup | jq
# Attendu : {"completed":true}
3. /setup via la SPA
Ouvrir http://127.0.0.1:8080/setup dans le navigateur. Le formulaire affiche :
- Install token (paste depuis les logs)
- Admin email
- Display name (optional)
- Password + Confirm password (≥ 8 chars)
Le bouton « Create admin » appelle POST /api/v1/setup puis redirige vers /login. Si la stack a déjà un admin, la page affiche « Already done · Go to login → ».
4. Login + /auth/me
LOGIN=$(curl -s -c /tmp/cookies.txt -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@metamorph.local","password":"AdminPass1234!"}')
echo "$LOGIN" | jq
ACCESS=$(echo "$LOGIN" | jq -r .access_token)
curl -s http://localhost:8080/api/v1/auth/me -H "Authorization: Bearer $ACCESS" | jq
# Attendu : {is_admin: true, groups: ["admin"], email: "...", ...}
Le cookie metamorph_refresh est dans /tmp/cookies.txt (HTTPOnly, scope /api/v1/auth/).
Côté SPA : /login → email + password → redirige vers /. Le header affiche l'email courant via le testid me-email (<span data-testid="me-email">…</span>).
5. Refresh rotation
curl -s -b /tmp/cookies.txt -c /tmp/cookies.txt -X POST http://localhost:8080/api/v1/auth/refresh | jq
# Attendu : nouveau access_token, le cookie refresh est rotaté
L'ancien refresh token devient revoked_at IS NOT NULL en base ; un 2e usage du même refresh révoque la chaîne entière (détection de réutilisation).
6. Invitation → register → 2e login
# Admin crée invitation
INV=$(curl -s -X POST http://localhost:8080/api/v1/invitations \
-H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" \
-d '{"email_hint":"alice@metamorph.local"}')
echo "$INV" | jq
INV_TOKEN=$(echo "$INV" | jq -r .token)
# Preview (anonyme)
curl -s "http://localhost:8080/api/v1/invitations/preview/$INV_TOKEN" | jq
# Acceptance
curl -s -X POST "http://localhost:8080/api/v1/invitations/accept/$INV_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"alice@metamorph.local","password":"AlicePass1234!"}' | jq
# Login Alice
curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@metamorph.local","password":"AlicePass1234!"}' | jq -r .access_token | head -c 40
Côté SPA : ouvrir http://127.0.0.1:8080/register?token=<INV_TOKEN>. La page affiche le hint email, demande password + confirm, redirige vers /login après acceptance.
7. RBAC — non-admin reçoit 403
ALICE_ACCESS=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@metamorph.local","password":"AlicePass1234!"}' | jq -r .access_token)
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://localhost:8080/api/v1/invitations \
-H "Authorization: Bearer $ALICE_ACCESS" -H "Content-Type: application/json" -d '{}'
# Attendu : HTTP 403
8. Change password (force logout-all)
curl -s -X POST http://localhost:8080/api/v1/auth/change-password \
-H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" \
-d '{"current_password":"AdminPass1234!","new_password":"AdminPass5678!"}' | jq
# Attendu : {"ok":true}
Tous les refresh tokens du user sont révoqués → toute autre session existante repasse par /login.
9. Profile (SPA)
Une fois loggué, ouvrir /profile :
- Carte Identity (email, display name, locale)
- Carte Groups (tags)
- Carte Permissions (admin → tag « ADMIN — bypasses checks »)
- Carte Change password (current + new + confirm → submit → redirige vers /login après 1.5 s)
Logout via le bouton du header → redirige vers /login. Tenter /profile sans session → redirige vers /login.
10. Tests automatisés
10.1 — Backend pytest
make test-api
Attendu : 24 passed (1 health + 8 schema + 15 auth flow).
Détail M2 (tests/test_auth_flow.py) :
| Test | Couvre |
|---|---|
| setup_status_starts_uncompleted | /setup état initial |
| setup_creates_first_admin | bootstrap consomme le token, crée admin |
| setup_status_now_completed | idempotence |
| setup_replay_is_blocked | 409 si déjà fait |
| login_and_me | flux login → /me complet |
| login_with_wrong_password_returns_401 | aucune énumération |
| me_without_token_returns_401 | bearer requis |
| refresh_rotates_and_old_token_is_revoked | rotation chaîne |
| refresh_with_no_cookie_returns_401 | cookie obligatoire |
| logout_clears_cookie_and_is_idempotent | idempotence |
| admin_creates_invitation_and_invitee_accepts | flux complet |
| unauthenticated_cannot_create_invitation | auth requis |
| non_admin_cannot_create_invitation | RBAC 403 |
| used_invitation_cannot_be_accepted_twice | usage unique |
| change_password_revokes_all_refresh_tokens | logout-all |
10.2 — Playwright e2e
make e2e
Attendu : 20 passed (8 M0 + 4 M1 + 8 M2).
Détail M2 (e2e/tests/m2-auth.spec.ts) :
| Test | Couvre |
|---|---|
| setup status is uncompleted before bootstrap | API contract |
| SPA setup form creates the first admin | UI /setup |
| SPA login works and reveals the profile page | UI /login + /profile |
| admin issues an invitation via the API and the front renders the registration form | UI /register?token=… |
| invitee submits the registration form and can log in | UI register accept |
| non-admin gets 403 on the admin invitations endpoint | RBAC |
| refresh token rotation works through the SPA | rotation |
| logout clears the session and redirects to login | logout |
Le rapport HTML : e2e/playwright-report/index.html. JUnit : e2e/playwright-report/junit.xml.
11. Pièges connus (M2 spécifiques)
- Cookie
Secureen HTTP :secure=Truefait rejeter le cookie par le browser sur HTTP. Métamorph utilisesecure = APP_ENV in ("prod","staging")— endev/testle cookie est non-secure. En prod, le reverse proxy externe doit terminer la TLS. pydantic.EmailStrrejette les TLD réservés (.local,.corp,.test) avecglobally_deliverable=True. Métamorph utilise un typeEmailpermissif via regex (cf.app/api/_validation.py).getByLabelPlaywright récupère le nom accessible de l'input. Si la<label>enveloppe l'input ET le hint, le hint pollue le nom → matchs ratés. Le composantTextFieldmet le hint en sibling, pas à l'intérieur du<label>.- Rate-limit
/auth/login(10/min) : peut bloquer une suite de tests.flask-limiterest désactivé par construction quandAPP_ENV=test. /diag/resetest exposé seulement quandAPP_ENV in ("dev","test"). Truncate les tables auth + force-mint un nouveau token install. Ne jamais activer en prod.- Compose recreate :
make rebuildne recrée pas les containers — il fautmake down && make upaprès un changement front pour que nginx serve le nouveau bundle.
12. Teardown
make down
# ou full reset (DESTRUCTEUR) :
make clean