# Spec — Tests End-to-End Playwright (Studio-m)

> Statut : proposition / à valider. Périmètre volontairement large — l'implémentation se fera en
> plusieurs sessions (voir §9 Phasage). Cette spec est fondée sur l'état réel du code au 2026-07-20.

## 1. Objectif & principes

Mettre en place une suite de tests end-to-end **Playwright** couvrant la majorité des interfaces du
site, exécutée contre une **base de données de test isolée** (Docker), avec **tous les effets de bord
externes désactivés** (emails Brevo, télémétrie OTEL, cron disque). Deux exigences spécifiques :

1. **Couverture large des interfaces** (listes, formulaires create/edit/delete, galeries client,
   config, journal emails).
2. **Vérification des invalidations TanStack Query** : après chaque mutation de formulaire, les bonnes
   query keys (centralisées récemment dans `packages/common/src/api/query-keys.ts`) doivent être
   invalidées et l'UI doit refléter le changement sans rechargement.

Principes :
- **Isolation totale** : jamais contre la base de dev (`studio-m` sur port 3309) ni la vraie API Brevo.
- **Déterminisme** : chaque test (ou fichier) part d'un état DB connu ; pas de dépendance à l'ordre.
- **Garde-fou anti-emails** : un test ne doit jamais pouvoir déclencher un vrai envoi Brevo, même par
  erreur (voir §3 — le garde actuel est *commenté*, les emails partent aujourd'hui quel que soit
  `NODE_ENV`).

---

## 2. Architecture de l'environnement de test

### 2.1 Vue d'ensemble

```
Playwright  →  Front Vite (VITE_API_URL=http://127.0.0.1:3011)
                    │
                    ▼
              API NestJS de test  (PORT=3011)
                    │  DATABASE_URL → mysql://root@localhost:3310/studio-m-test
                    │  MAIL_DISABLED=true, pas d'OTEL, UPLOAD_PATH=dossier jetable
                    ▼
              MySQL 8.0 (docker-compose.test.yml, port 3310)
```

On sépare **strictement** les ports de la stack de dev existante :

| Composant        | Dev (existant)        | Test (nouveau)          |
|------------------|-----------------------|-------------------------|
| MySQL            | 3309 (`mysql:5.7`)    | **3310** (`mysql:8.0`)  |
| API NestJS       | 3001                  | **3011**                |
| Front Vite       | 5173                  | **5174** (ou preview)   |
| Base de données  | `studio-m`            | `studio-m-test`         |

> Conformité CLAUDE.md : le compose de dev actuel utilise `mysql:5.7` ; **le nouveau compose de test
> utilisera `mysql:8.0`** comme imposé par les standards Docker. Migration 5.7→8.0 à valider côté
> schéma (charset `utf8mb4_unicode_ci` déjà en place, pas de blocage anticipé).

### 2.2 `docker-compose.test.yml` (à créer, racine)

- Service `db-test` : image `mysql:8.0`, port `3310:3306`, `MYSQL_ALLOW_EMPTY_PASSWORD: "yes"`,
  `MYSQL_DATABASE: studio-m-test`, charset `utf8mb4` / collation `utf8mb4_unicode_ci`.
- **Pas** de montage de `docker/mysql-init` (le dump de prod ne doit pas polluer la base de test).
- Volume nommé dédié (`db_test_data`) ou `tmpfs` pour repartir de zéro rapidement.
- Healthcheck `mysqladmin ping` (comme le compose dev).
- Pas d'Adminer requis (optionnel pour debug, sur un port distinct type 8084).

### 2.3 Fichier `.env.test`

Aucun `.env.example`/`.env.test` n'existe aujourd'hui. On crée **`apps/api/.env.test`** :

```
DATABASE_URL="mysql://root:@localhost:3310/studio-m-test"
PORT=3011
JWT_SECRET="test-secret-not-for-prod"      # requis : le login échoue sans lui
UPLOAD_PATH="./.tmp-test-uploads"          # dossier jetable, git-ignoré
                                           # emails : FakeEmailService injecté en test (voir §3.1)
FRONT_SITE="http://127.0.0.1:5174"
ORIGIN_SITE="http://127.0.0.1:3011"
# NE PAS définir OTEL_EXPORTER_OTLP_ENDPOINT → télémétrie off proprement
# NE PAS définir BREVO_API_KEY → aucune fuite possible
```

Et côté front, **`apps/front/.env.test`** : `VITE_API_URL=http://127.0.0.1:3011`.

> ⚠️ Sécurité repérée hors périmètre : la vraie `BREVO_API_KEY` est en clair dans `apps/api/.env`
> (gitignoré mais présent sur disque). À faire tourner/révoquer indépendamment de cette spec.

---

## 3. Désactivation des effets de bord (bloquant — à faire AVANT tout run)

### 3.1 Emails (Brevo) — priorité absolue

**Décision retenue : service email de test injecté par DI, actif en e2e uniquement.** Pas de flag
`MAIL_DISABLED` dans le code produit, pas de `NODE_ENV` en dur dans le service. On isole l'envoi
derrière une abstraction et on substitue l'implémentation en test.

État actuel : `apps/api/src/modules/emails/services/brevo.service.ts` envoie via `fetch` sur
`https://api.brevo.com/v3/smtp/email`. Le garde `NODE_ENV` existe mais est **commenté** (lignes ~44-48
et ~100-104) → **les emails partent réellement, quel que soit l'environnement**. On supprime ce garde
commenté et on remplace la stratégie par l'injection.

Implémentation :
1. **Interface `EmailSender`** (token DI) avec `sendTemplateEmail` / `sendHtmlEmail`. `BrevoService`
   l'implémente (prod). Les use cases `ShareShooting.ts` / `ShareCustomerGallery.ts` dépendent de
   l'interface, pas de `BrevoService` en dur.
2. **`FakeEmailService`** (`apps/api/test/fakes/fake-email.service.ts`) : implémente `EmailSender`,
   **n'envoie rien**, **enregistre en mémoire** les emails « envoyés » (to, template, params). Permet
   d'**asserter** qu'un partage a bien déclenché un email sans aucun appel réseau.
3. **Injection en e2e uniquement** :
   - API in-process (jest-e2e) : `overrideProvider(EmailSender).useClass(FakeEmailService)`.
   - API réelle pilotée par Playwright (process séparé) : provider conditionnel dans `EmailModule`
     (`useClass: NODE_ENV === 'test' ? FakeEmailService : BrevoService`) **ou** entrypoint de test
     dédié qui monte le module avec le fake. Le `FakeEmailService` expose les emails capturés via un
     **endpoint de test** (`GET /__test__/emails`, monté seulement si `NODE_ENV === 'test'`) pour que
     Playwright vérifie côté navigateur qu'un partage a « envoyé » le bon email.

**Test de non-régression** : appeler l'endpoint de partage et vérifier (a) aucun appel réseau Brevo,
(b) le `FakeEmailService` a bien capturé l'email attendu (destinataire, template), (c) réponse OK.

### 3.2 Télémétrie OTEL

Déjà désactivable proprement : tout est encapsulé dans `if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT)`
(`apps/api/src/otel.ts`). Il suffit de **ne pas définir** cette variable en test. Rien à coder.

### 3.3 Cron / disk-monitor

`disk-monitor.service.ts` : `@Cron(EVERY_HOUR)` + exécution `onApplicationBootstrap` (statfs + logs,
pas d'appel externe). Inoffensif mais bruyant. Optionnel : gérer via un flag `DISABLE_CRON` ou laisser
tel quel (aucun effet externe). **Non bloquant.**

### 3.4 Uploads fichiers

Écriture disque locale sous `UPLOAD_PATH` (Fastify static + `fastify-multer` + `sharp`). En test :
pointer `UPLOAD_PATH` vers `./.tmp-test-uploads` (git-ignoré), nettoyé entre les runs. Pas de stockage
cloud → rien d'autre à neutraliser.

### 3.5 Récapitulatif effets de bord

| Effet de bord        | Mécanisme test                                  | À coder ?          |
|----------------------|-------------------------------------------------|--------------------|
| Emails Brevo         | interface `EmailSender` + `FakeEmailService` (DI) | **Oui** (abstraction) |
| Télémétrie OTEL      | ne pas définir `OTEL_EXPORTER_OTLP_ENDPOINT`    | Non                |
| Cron disque          | inoffensif, flag optionnel                      | Non (optionnel)    |
| Uploads              | `UPLOAD_PATH` jetable                           | Non (config)       |
| Health self-ping     | `fetch(ORIGIN_SITE/health)` → boucle locale     | Non                |

---

## 4. Données de test, fixtures & bootstrap admin

Contexte : **pas de `prisma/migrations`** (workflow `prisma db push`), **pas de seed Prisma**. La base
de test est vide au départ.

### 4.1 Préparation du schéma

`DATABASE_URL=<test> npx prisma db push` (crée les tables sur la base vide). À intégrer au global
setup Playwright et au script npm.

### 4.2 Bootstrap du premier admin (problème d'amorçage)

`POST users` (`CreateUser`) est protégé par l'`AuthGuard` global → impossible de créer le tout premier
utilisateur via l'API sans être déjà authentifié. Options :

- **Recommandé** : script de seed de test qui **insère directement via Prisma** un `User`
  `role='ADMIN'` (et un `SUPER_ADMIN`) avec `password` = `hashPassword(...)` (bcrypt 10 rounds, cf.
  `apps/api/src/models/users/functions/password.functions.ts`). → `apps/api/test/seed.ts`.
- Alternative : `@Public()` temporaire sur CreateUser (déconseillé, change le comportement prod).

### 4.3 Stratégie de fixtures par test

- **Reset entre fichiers** : truncate de toutes les tables + re-seed admin, via une fonction utilitaire
  (`resetDb()`), appelée en `beforeEach`/`beforeAll` selon le coût.
- **Données créées par l'UI** : privilégier la création via les parcours testés (un test « créer
  client » sert de fixture au test « éditer client ») quand c'est le sujet ; sinon seed direct pour
  aller vite.
- **Auth dans les tests** : deux modes
  - parcours login réel (couvre `LoginForm`) ;
  - **bypass** pour les autres tests : `localStorage.setItem("token_studio", <jwt>)` (clé confirmée)
    via `storageState` Playwright, JWT signé avec `JWT_SECRET` de test → pas de login répété.

---

## 5. Setup Playwright

### 5.1 Emplacement & config

- Nouveau dossier `apps/front/e2e/` (ou `e2e/` racine) avec `playwright.config.ts`.
- `webServer` Playwright : démarre le front en mode test (`VITE_API_URL` → 3011). L'API et MySQL de
  test sont démarrés par le global setup (voir 5.3) — Playwright ne sait pas les gérer nativement.
- Projects : au minimum `chromium` ; l'app est mobile-first (Ionic/Capacitor) → **ajouter un viewport
  mobile** (ex. iPhone 12) en plus du desktop pour les vues client.
- `storageState` : un fichier d'auth admin généré au global setup pour les tests authentifiés ;
  contexte vierge (sans storageState) pour les vues publiques.

### 5.2 Sélecteurs — décision retenue : `data-testid`

**Aucun `data-testid` n'existe** aujourd'hui dans `apps/front/src` ni `packages/common/src`. On en
**ajoute progressivement** sur les éléments testés (inputs Ionic, boutons submit, lignes de liste,
lignes de tableau). Convention : `data-testid` kebab-case et parlant
(`shooting-form-name`, `customer-row`, `login-submit`…). Config Playwright :
`use: { testIdAttribute: 'data-testid' }` → sélection via `getByTestId(...)`. Les testids sont ajoutés
au fil des phases, au moment d'écrire chaque spec (pas de big-bang).

Note Ionic : `ion-input`/`ion-button` encapsulent des shadow inputs → prévoir des helpers de saisie
(`fill` sur l'input interne) et attendre les transitions de page Ionic.

### 5.3 Orchestration (global setup / teardown)

1. `docker compose -f docker-compose.test.yml up -d` + attente healthcheck.
2. `prisma db push` sur la base de test.
3. Seed admin (`test/seed.ts`).
4. Démarrer l'API NestJS de test (`NODE_ENV=test`, `.env.test`, port 3011) — process géré par le
   setup, arrêté au teardown.
5. Générer le `storageState` admin (login ou injection token).
6. Teardown : stopper l'API, `docker compose down -v`, nettoyer `./.tmp-test-uploads`.

Scripts npm proposés (racine + `apps/api` / `apps/front`) :
```
test:e2e:up      docker compose -f docker-compose.test.yml up -d --wait
test:e2e:db      DATABASE_URL=<test> prisma db push && ts-node apps/api/test/seed.ts
test:e2e:api     NODE_ENV=test env-cmd -f apps/api/.env.test node dist/main   (ou nest start)
test:e2e         playwright test
test:e2e:down    docker compose -f docker-compose.test.yml down -v
```

> `apps/api` déclare déjà un script `test:e2e` (`jest --config ./test/jest-e2e.json`) **mais le dossier
> `test/` n'existe pas**. On peut réutiliser ce couloir pour les tests d'API in-process (invalidations
> côté serveur, garde emails) et Playwright pour le navigateur.

---

## 6. Matrice de couverture (interfaces)

Légende priorité : **P0** = cœur métier / bloquant, **P1** = important, **P2** = secondaire.

### 6.1 Auth & session — P0
- Login succès (admin) → redirection `/shootings`, token en localStorage.
- Login échec (mauvais mdp) → toast d'erreur, reste sur `/login`.
- Guard `requiresAuth` : accès `/shootings` sans token → redirige `/login`.
- Guard `requiresGuest` : `/login` en étant connecté → redirige `/`.
- Logout → token supprimé, retour login.
- Expiration token (`message === "Token expired"`) → logout auto (mock réponse 401).

### 6.2 Shootings — P0
- Liste `/shootings` : affichage, recherche (`POST shootings/search`), filtres.
- Créer `/shootings/create` : formulaire (name, date→description ISO, allowDownload, masonryMode,
  client imbriqué **ou** sélection client existant) → apparaît dans la liste **sans reload**
  (invalidation `shootings.all` + `shootings.details`).
- Éditer `/shootings/:id` (même composant `CreateShooting.vue`) → modifications reflétées.
  ⚠️ **Voir §7 bug d'invalidation update.**
- Détail `/shootings/:id/detail` : upload photos, suppression, réordonnancement, favoris.
- Supprimer un shooting → disparaît de la liste.
- Dupliquer → nouvelle entrée dans la liste.
- Partager (`ShareShooting`) → **sans envoi réel** ; vérifier le garde emails.

### 6.3 Customers — P0
- Liste `/customers`, recherche.
- Créer `/customers/create` (firstname, lastname, email, phone, address, zipcode, city) → liste MAJ.
- Éditer `/customers/edit/:id` (`CustomerDetail.vue`) → MAJ reflétée.
- Supprimer → disparaît (invalidation `customers.all`).
- Vue `/customers/:id/view`.

### 6.4 Users — P1
- Liste `/users` (`use-search-users`).
- Créer `/users/create` (firstname, lastname, email, password, role USER/ADMIN) → liste MAJ
  (invalidation `users.search` — c'était le bug fixé par le commit récent, à verrouiller par un test).
- Éditer `/users/edit/:id`.
- Changer mot de passe `/update-password` → succès, form reset (pas d'invalidation attendue).

### 6.5 Vues client publiques — P1 (viewport mobile)
- `/shootings/:id/view` : galerie partagée (sans auth).
- `/my-shootings/:customerId` : espace client.
- `/favorites` : favoris (toggle favori → `shootings.favorites`).
- `/infos` : page publique.

### 6.6 Config & Emails — P2
- `/config` : édition des messages par défaut (`PUT config/default-messages`).
  ⚠️ Pas de `requiresAuth` sur la route (autorisation API only) — tester le comportement.
  ⚠️ N'invalide pas `appConfig.defaultMessages` (voir §7).
- `/emails` : journal (`POST emails/search`), filtres, pagination.
- `/pwas` : gestion PWA (P2, à cadrer).

---

## 7. Vérification des invalidations TanStack Query

C'est une exigence explicite. Deux couches complémentaires :

### 7.1 Couche unit/intégration (rapide, ciblée)
Pour **chaque mutation**, spy sur `queryClient.invalidateQueries` et assertion de la clé attendue
(issue de `queryKeys`). Attention au **matching par préfixe** TanStack : `shootings.details`
(`["shooting"]`) couvre tous les `shootings.detail(id)` ; `users.activities.all` couvre `list`/`count`.

Table de référence (comportement **actuel** constaté) :

| Mutation | Fichier | Invalide aujourd'hui | Attendu |
|---|---|---|---|
| Delete customer | `use-delete-customer.ts` | `customers.all` | ✅ |
| Create/Update customer | `use-create-customer.ts` | `customers.all` | ✅ |
| Create user activity | `use-create-user-activity.ts` | `users.activities.all` | ✅ |
| Delete shooting | `use-delete-shooting.ts` | `shootings.all` | ✅ |
| Duplicate shooting | `use-duplicate-shooting.ts` | `shootings.all` | ✅ |
| Toggle favorite | `use-toggle-favorite.ts` | `shootings.favorites` | ⚠️ voir bug 5 |
| Delete photos | `use-delete-photos.ts` | `shootings.details` | ⚠️ voir bug 6 |
| Create/Update shooting | `use-create-shooting.ts` | `shootings.all` + `shootings.details` | ✅ |
| Create/Update user | `use-create-user.ts` | `users.search` | ✅ (fix récent) |

### 7.2 Couche e2e (comportement observable)
Après une mutation via formulaire, vérifier que **l'UI se met à jour sans reload** (nouvelle ligne,
valeur éditée, élément disparu). C'est le vrai test d'invalidation du point de vue utilisateur.

### 7.3 Bugs d'invalidation à corriger (décision retenue : **tout corriger maintenant**)

L'exploration a relevé 7 incohérences réelles. **On les corrige toutes** avant/pendant l'écriture des
tests, puis les tests figent le comportement **corrigé**. Chaque correction est accompagnée d'un test
(unit spy `invalidateQueries` §7.1 + e2e UI-sans-reload §7.2).

Correctifs (tous à appliquer) :

1. **`use-update-shooting.ts` (CRITIQUE)** — n'invalide **rien**. → invalider `shootings.all` +
   `shootings.details` (aligné sur la création).
2. **`use-multiple-photo-upload.ts`** — après upload, aucune invalidation. → invalider
   `shootings.details` (symétrie avec `use-delete-photos.ts`).
3. **`use-toggle-favorite.ts`** — invalide `shootings.favorites` seulement. → ajouter
   `shootings.details` (état favori du détail).
4. **`use-update-photos-order.ts`** — réordonnancement sans invalidation. → invalider
   `shootings.details` (vérifier au passage qu'aucun refetch manuel de l'appelant ne fait doublon).
5. **`use-delete-photos.ts`** — invalide `shootings.details` mais pas `shootings.all`. → ajouter
   `shootings.all` (compteur de photos dans la liste).
6. **`Config.vue` (mutation inline)** — `PUT config/default-messages` n'invalide pas
   `appConfig.defaultMessages`. → extraire dans un composable + invalider `appConfig.defaultMessages`.
7. **Convention `shootings.all` vs `shootings.details`** — harmoniser : toute mutation modifiant un
   shooting/ses photos invalide **`shootings.all` (liste) ET `shootings.details` (détail)**, sauf cas
   justifié. Appliquer ce principe uniformément à create/update/delete/duplicate/photos.

---

## 8. Structure de fichiers cible

```
docker-compose.test.yml                      # MySQL 8.0 :3310, base studio-m-test
apps/api/.env.test                           # DATABASE_URL test, JWT_SECRET, MAIL_DISABLED...
apps/front/.env.test                         # VITE_API_URL=http://127.0.0.1:3011
apps/api/test/
  seed.ts                                    # bootstrap admin + super_admin via Prisma
  reset-db.ts                                # truncate + re-seed
  jest-e2e.json                              # tests API in-process (garde emails, invalidations serveur)
apps/front/e2e/
  playwright.config.ts
  global-setup.ts / global-teardown.ts       # docker up, db push, seed, storageState, API start
  fixtures/                                   # helpers auth, resetDb, factories
  auth.spec.ts
  shootings.spec.ts
  customers.spec.ts
  users.spec.ts
  client-views.spec.ts                       # vues publiques, viewport mobile
  config-emails.spec.ts
  invalidations.spec.ts                      # vérifs UI-sans-reload
```

---

## 9. Phasage (plusieurs sessions)

**Phase 0 — Fondations (bloquant, à faire en premier)**
- [ ] Emails : interface `EmailSender`, `BrevoService implements EmailSender`, use cases dépendant de
      l'interface, `FakeEmailService` (in-memory) injecté en test + endpoint `GET /__test__/emails` +
      test de non-régression. Supprimer le garde `NODE_ENV` commenté.
- [ ] `docker-compose.test.yml` (MySQL 8.0, port 3310).
- [ ] `apps/api/.env.test` + `apps/front/.env.test` + `.gitignore` (`.tmp-test-uploads`).
- [ ] `apps/api/test/seed.ts` + `reset-db.ts` (bootstrap admin).

**Phase 1 — Harnais Playwright**
- [ ] `playwright.config.ts`, global setup/teardown, orchestration API+DB, `storageState` admin.
- [ ] Scripts npm `test:e2e:*`.
- [ ] `auth.spec.ts` (valide le harnais de bout en bout).

**Phase 2 — Cœur métier (P0)** — chaque correctif d'invalidation §7.3 est fait juste avant le test
qui le couvre.
- [ ] Correctifs invalidation shootings §7.3 (1,2,3,4,5,7) + `shootings.spec.ts`.
- [ ] `customers.spec.ts`.

**Phase 3 — Reste (P1/P2)**
- [ ] `users.spec.ts` (verrouille le fix d'invalidation `users.search`).
- [ ] `client-views.spec.ts` (mobile).
- [ ] Correctif invalidation config §7.3 (6) + `config-emails.spec.ts`, `pwas`.

**Phase 4 — Invalidations transverses & robustesse**
- [ ] `invalidations.spec.ts` + tests unit spy `invalidateQueries` (table §7.1 corrigée).
- [ ] Intégration CI (compose test + Playwright).

---

## 10. Décisions arrêtées

1. **Emails** : ✅ interface `EmailSender` + `FakeEmailService` in-memory injecté en e2e uniquement
   (pas de flag dans le code produit). Garde `NODE_ENV` commenté supprimé. Cf. §3.1.
2. **Sélecteurs** : ✅ `data-testid` ajoutés progressivement. Cf. §5.2.
3. **Bugs d'invalidation §7.3** : ✅ **tous corrigés** au fil des phases, chaque correctif couvert par
   un test.

Restent ouvertes (non bloquantes pour la Phase 0) :
4. **Emplacement Playwright** : `apps/front/e2e/` (proche du front) ou `e2e/` racine ? — défaut
   proposé : `apps/front/e2e/`.
5. **CI** : cible d'exécution (GitHub Actions ? runner local ?) — impacte le global setup Docker.
