f45b96d147
Section E (E1-E7) covers the fixes that had none: reauth inline retry, recovery setup + redeem on argon2id-v2, recovery commit-after-save, the sessionStorage key removal, and a slideover regression guard after deleting the dead entry modal. Dropped the now-done "legacy entry modal" item from the remaining list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
246 lines
14 KiB
Markdown
246 lines
14 KiB
Markdown
# Scénarios de test — écriture concurrente & fermeture de l'éditeur
|
|
|
|
Sessions du 2026-07-22. Tous les tests sont **manuels** (ils portent sur des
|
|
interactions UI que le harness `node:test` ne couvre pas). Rebuild obligatoire
|
|
avant de commencer : `delphi-backend\assets\BuildAssets.cmd` puis build Delphi.
|
|
|
|
Convention : **PRÉ** = état de départ, **ACTION**, **ATTENDU**. Un test qui
|
|
échoue = régression, noter le commit courant.
|
|
|
|
---
|
|
|
|
## Les 4 classes de bug
|
|
|
|
| # | Classe | Cause racine | Corrigé par |
|
|
|---|---|---|---|
|
|
| **A** | Écriture extérieure écrasée au Save | L'éditeur garde le payload chargé à l'ouverture et le re-poste **entier** ; toute écriture faite ailleurs entre-temps est perdue | `editorAbsorbChange` (d146fbe) |
|
|
| **B** | Éditeur fermé sans garde-fou | `closeSlideOver()` appelé sans condition + click-outside non filtré | `closeEditorForDeleted` (3dbce78), whitelist (e1ddc65) |
|
|
| **C** | PUT `/entries` sans `withEncryptedMeta` | Les colonnes en clair sont droppées côté serveur → le body ne contient rien de stockable et blanchit les `*_enc` | d146fbe |
|
|
| **D** | UI construite sans regarder la vue | Le menu kebab est partagé entre vues ; la corbeille n'était pas testée | d146fbe |
|
|
|
|
---
|
|
|
|
## A — Éditeur ouvert vs écriture extérieure
|
|
|
|
Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le tag
|
|
`old`.
|
|
|
|
### A1 · batch Add tag sur l'entrée éditée
|
|
- **PRÉ** : `A` ouverte dans l'éditeur, taper du texte dans NOTE/PASSWORD (ne pas sauver)
|
|
- **ACTION** : cocher `A` + `B` → barre de sélection → *Add tag* → `shared` → puis **Save** dans l'éditeur
|
|
- **ATTENDU** : le chip `shared` apparaît **immédiatement** dans l'éditeur ; après Save, `A` porte `old, shared` **et** la modif tapée. Le bouton Save reste visible (l'entrée était déjà dirty) mais le tag seul ne rend pas dirty.✅
|
|
|
|
### A2 · batch Add tag sur une **autre** entrée
|
|
- **PRÉ** : `A` ouverte + dirty
|
|
- **ACTION** : cocher `B` + `C` seulement → *Add tag* → `x` → Save dans l'éditeur
|
|
- **ATTENDU** : l'éditeur ne bouge pas (aucun chip ajouté), `A` conserve ses tags, `B`/`C` taguées.✅
|
|
|
|
### A3 · batch Move to folder sur l'entrée éditée
|
|
- **ACTION** : `A` ouverte + dirty → cocher `A` → *Move to folder…* → `Perso` → Save
|
|
- **ATTENDU** : le select Folder de l'éditeur bascule sur `Perso` tout seul ; après Save l'entrée est bien dans `Perso`.✅
|
|
|
|
### A4 · dossier choisi dans l'éditeur **puis** move extérieur
|
|
- **ACTION** : `A` ouverte, changer Folder → `Archive` (ne pas sauver) → cocher `A` → *Move to folder…* → `Perso` → Save
|
|
- **ATTENDU** : `Perso` gagne (action extérieure = intention la plus récente). Comportement documenté, pas un bug. ✅
|
|
|
|
### A5 · glisser une entrée sur un dossier (`moveEntryToFolder`)
|
|
- **ACTION** : `A` ouverte + dirty → glisser la ligne `A` sur `Perso` dans la sidebar → Save
|
|
- **ATTENDU** : identique à A3.✅
|
|
|
|
### A6 · glisser une **multi-sélection** sur un dossier (`moveEntriesToFolder`) ⚠️
|
|
- **ACTION** : cocher `A` + `B` → glisser sur `Perso` → recharger (F5 impossible : relancer l'app)
|
|
- **ATTENDU** : dossier changé **et** nom / username / site / tags **intacts**.
|
|
- **Historique** : ce chemin n'appelait pas `withEncryptedMeta` → il blanchissait les métadonnées chiffrées. Si des entrées ont été déplacées ainsi avant le fix, elles sont peut-être déjà vides — vérifier.✅
|
|
|
|
### A7 · suppression du dossier de l'entrée éditée
|
|
- **ACTION** : `A` (dossier `Work`) ouverte + dirty → sidebar → supprimer le dossier `Work` → Save
|
|
- **ATTENDU** : le select de l'éditeur passe à `All` ; après Save l'entrée est dans `All`, le dossier supprimé **ne réapparaît pas**.✅
|
|
|
|
### A8 · tags édités des deux côtés
|
|
- **ACTION** : `A` ouverte, ajouter le chip `mine` dans l'éditeur (ne pas sauver) → batch *Add tag* `shared` sur `A` → Save
|
|
- **ATTENDU** : `old, mine, shared` — aucun des trois perdu.✅
|
|
|
|
### A9 · favori / épingle pendant l'édition (non-régression)
|
|
- **ACTION** : `A` ouverte + dirty → cliquer ★ et 📌 sur la ligne `A` → Save
|
|
- **ATTENDU** : l'entrée reste favorite et épinglée (ces colonnes ne sont pas dans le PUT de l'éditeur).✅
|
|
|
|
### A10 · icône / pièce jointe pendant l'édition (non-régression)
|
|
- **ACTION** : `A` ouverte + dirty → Settings → *Fetch missing* favicons (ou upload d'icône) → Save
|
|
- **ATTENDU** : l'icône survit au Save (endpoint dédié, hors payload éditeur).✅ icon not refreshed in edit
|
|
|
|
### A10b · icône rafraîchie dans l'éditeur ouvert
|
|
- **ACTION** : `A` ouverte → Settings → *Fetch missing* / *Re-fetch all* favicons
|
|
- **ATTENDU** : l'aperçu d'icône **dans l'éditeur** se met à jour tout seul (pas seulement la carte). Idem après un import en mode overwrite qui apporte une icône.
|
|
|
|
### A11 · sync / import pendant que l'éditeur est ouvert
|
|
- **ACTION** : `A` ouverte + dirty → lancer *Sync now* (ou importer un JSON en mode overwrite) qui met `A` à jour → Save
|
|
- **ATTENDU** : modale « Entry changed elsewhere ». **Annuler ⇒ rien n'est écrit** (la version du sync/import survit, l'éditeur reste ouvert le temps de recopier). **Overwrite ⇒** la version de l'éditeur gagne.
|
|
|
|
### A12 · pas de fausse alerte (non-régression d'A11)
|
|
Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » :
|
|
- **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence)
|
|
- **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement)
|
|
- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7)
|
|
- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur)
|
|
- **A12e** : `A` ouverte + dirty → sync **sans** changement distant sur `A` → Save
|
|
|
|
### A13 · entrée supprimée ailleurs pendant l'édition
|
|
- **ACTION** : `A` ouverte + dirty → un sync distant supprime `A` (tombstone) → Save
|
|
- **ATTENDU** : pas de plantage. L'entrée n'est plus en mémoire → le contrôle A11 passe la main ; le PUT échoue proprement avec un toast d'erreur.
|
|
|
|
---
|
|
|
|
## B — Fermeture de l'éditeur & message de discard
|
|
|
|
Le message n'apparaît que si `confirmOnUnsaved` est actif **et** que l'entrée
|
|
est dirty. Tester avec `askBeforeDelete` **ON puis OFF** (Settings → Security).
|
|
|
|
### B1 · ✕ sur une **autre** ligne
|
|
- **PRÉ** : `A` ouverte + dirty
|
|
- **ACTION** : cliquer ✕ (Move to trash) sur la ligne `B`
|
|
- **ATTENDU** : `B` part à la corbeille, l'éditeur de `A` **reste ouvert** avec les modifs. Vrai dans les deux réglages de `askBeforeDelete`.
|
|
|
|
### B2 · ✕ sur la ligne éditée, `askBeforeDelete` = OFF
|
|
- **ACTION** : `A` ouverte + dirty → ✕ sur `A`
|
|
- **ATTENDU** : modale « Discard unsaved changes? ». **Annuler ⇒ rien n'est supprimé** et l'éditeur reste ouvert. Confirmer ⇒ `A` à la corbeille + éditeur fermé.
|
|
|
|
### B3 · ✕ sur la ligne éditée, `askBeforeDelete` = ON
|
|
- **ATTENDU** : **une seule** modale (« Move to trash »). Pas de double prompt.
|
|
|
|
### B4 · batch Delete incluant l'entrée éditée
|
|
- **ACTION** : `A` ouverte + dirty → cocher `A` + `B` → *Delete*
|
|
- **ATTENDU** : une seule modale (« 2 entries will be moved to trash »), puis l'éditeur se ferme.
|
|
|
|
### B5 · batch Delete **excluant** l'entrée éditée
|
|
- **ATTENDU** : l'éditeur reste ouvert, modifs intactes.
|
|
|
|
### B6 · corbeille
|
|
- **ACTION** : ouvrir une entrée de la corbeille via *Trash actions*, puis : (a) *Delete forever* dessus, (b) batch purge la contenant, (c) *Empty trash*
|
|
- **ATTENDU** : dans les trois cas l'éditeur se ferme s'il portait une entrée purgée, sinon il reste.
|
|
|
|
### B7 · boutons de la barre de sélection
|
|
- **ACTION** : `A` ouverte + dirty → cocher `B` → cliquer successivement *Move to folder…*, *Add tag*, *Clear*
|
|
- **ATTENDU** : **aucun** de ces clics ne ferme l'éditeur ni n'affiche de discard.
|
|
|
|
### B8 · bouton *Empty trash* (barre de titre)
|
|
- **ATTENDU** : idem, pas de fermeture parasite de l'éditeur.
|
|
|
|
### B9 · chemins de fermeture légitimes (non-régression)
|
|
- Esc, ✕ de l'éditeur, clic sur le fond de la grille, ouverture d'une **autre** entrée
|
|
- **ATTENDU** : message de discard à chaque fois si dirty ; annuler garde l'éditeur ouvert sur la bonne entrée.
|
|
|
|
### B10 · verrouillage (comportement assumé)
|
|
- **ACTION** : `A` ouverte + dirty → auto-lock ou *Lock vault*
|
|
- **ATTENDU** : fermeture **sans** message, modifs perdues. Volontaire : la sécurité prime.
|
|
|
|
### B11 · Esc au-dessus d'un modal ouvert par-dessus l'éditeur
|
|
- **ACTION** : `A` ouverte + dirty → ouvrir un modal → Esc → Esc
|
|
- **ATTENDU** : 1ᵉʳ Esc ferme **le modal seul**, l'éditeur reste ouvert ; 2ᵉ Esc traite l'éditeur (discard).
|
|
- Modals à passer un par un : **+ New folder** (sidebar), **Customize folder**, *Change master password*, générateur de mot de passe, cheatsheet (`?`), historique de mot de passe, palette (`Ctrl+K`), outil TOTP, quick search (`Ctrl+Shift+Q`), **reauth** (déclenché par un export), **picker d'autofill** (`Ctrl+Shift+L` avec 2 entrées qui matchent).
|
|
- **Historique** : `#folderModal` (dynamique) n'était dans aucune liste → Esc fermait l'éditeur derrière. `#reauthModal`, `#quickSearchModal`, `#autofillPickerModal` (statiques) non plus.
|
|
|
|
### B12 · le modal reste fonctionnel après un Esc
|
|
- **ACTION** : ouvrir reauth (export) → Esc → **ré-ouvrir** (relancer l'export). Idem quick search et picker d'autofill.
|
|
- **ATTENDU** : le modal se ré-affiche normalement. Un Esc ne doit jamais **supprimer** un modal statique du DOM (sinon la fonctionnalité est morte jusqu'au redémarrage).
|
|
|
|
### B13 · clic sur un toast / l'overlay d'attente
|
|
- **ACTION** : `A` ouverte + dirty → lancer une action longue (export, sync) → cliquer sur le toast ou sur le spinner
|
|
- **ATTENDU** : l'éditeur ne se ferme pas, pas de discard.
|
|
|
|
---
|
|
|
|
## C — `withEncryptedMeta` sur tout write `/entries`
|
|
|
|
Test générique, à rejouer après **toute** modif touchant un body POST/PUT
|
|
`/entries` : faire l'action, **relancer l'app** (pour relire depuis SQLite),
|
|
vérifier que nom / site / username / tags s'affichent encore.
|
|
|
|
Chemins à couvrir : Save éditeur (neuf + existant), duplicate, import
|
|
(bulk + overwrite), sync (`applyRemoteSnapshot`), `addTagToEntry`,
|
|
`batchAddTag`, `moveEntryToFolder`, `moveEntriesToFolder`, `batchMoveToFolder`,
|
|
`migrateMetadataAtRest` (au unlock), rotation du master password.
|
|
|
|
Symptôme du bug : après relance, lignes au nom vide / `(no name)`.
|
|
|
|
---
|
|
|
|
## D — Vue corbeille
|
|
|
|
### D1 · menu kebab en table view
|
|
- **ACTION** : corbeille + vue table → kebab d'une ligne
|
|
- **ATTENDU** : `Restore` / `Copy…` / `Delete forever` **uniquement**. Pas de Pin / Favorite / Edit / Duplicate / Move to trash.
|
|
|
|
### D2 · vues cartes et liste
|
|
- **ATTENDU** : boutons restore + purge (déjà le cas avant), pas de drag possible.
|
|
|
|
### D3 · clic sur une ligne de corbeille
|
|
- **ATTENDU** : ouvre *Trash actions*, **jamais** l'éditeur (une entrée supprimée ne doit pas pouvoir être sauvegardée).
|
|
|
|
---
|
|
|
|
## E — Auth / recovery / mémoire (série 2026-07-25)
|
|
|
|
Ces flux touchent la dérivation de clé et l'anti-lockout : tester sur un compte
|
|
**argon2id-v2** (défaut courant). Rebuild **Delphi** requis (fixes serveur) +
|
|
BuildAssets pour le JS.
|
|
|
|
### E1 · reauth — mauvais master pw reste inline (les 4 flux)
|
|
- **ACTION** : sur chacun de *Set/Change PIN*, *Enable Quick Unlock*,
|
|
*Generate recovery code*, *Export* → taper un **mauvais** master pw.
|
|
- **ATTENDU** : le modal **reste ouvert** avec « Wrong master password. Attempt
|
|
N / 5. » ; au 5ᵉ échec ou 429 → toast lockout + fermeture. **Annuler** à tout
|
|
moment = action abandonnée, rien fait.
|
|
- **Historique** : PIN / Quick Unlock / recovery fermaient + toast au 1ᵉʳ essai.
|
|
|
|
### E2 · recovery setup sur compte argon2id-v2
|
|
- **ACTION** : *Generate recovery code* → bon master pw.
|
|
- **ATTENDU** : code généré, **pas** de « invalid password ».
|
|
- **Historique** : `/recovery-key/setup` rejetait tout compte -v2 / argon2id
|
|
(vérif serveur ne connaissait que pbkdf2 / pbkdf2-sha256).
|
|
|
|
### E3 · recovery generate → annuler n'écrit rien
|
|
- **PRÉ** : un code de recovery **existe déjà**.
|
|
- **ACTION** : *Regenerate code* → bon master pw → à l'affichage du code,
|
|
**Cancel** (ou Esc).
|
|
- **ATTENDU** : toast « Cancelled — no recovery code was set » ; l'**ancien**
|
|
code fonctionne toujours au redeem. Refaire avec **I saved it** → nouveau
|
|
code actif, ancien invalidé.
|
|
- **Historique** : le setup était POSTé avant l'affichage → annuler écrasait
|
|
quand même l'ancien code (chemin silencieux vers le lockout).
|
|
|
|
### E4 · recovery redeem sur compte argon2id-v2
|
|
- **ACTION** : générer un code, se déconnecter, *Use a recovery code* →
|
|
username + code.
|
|
- **ATTENDU** : déverrouillage OK, redirection vers *Change master password*.
|
|
Le coffre se déchiffre (clé dérivée avec les **bons** params Argon2 renvoyés
|
|
par le serveur).
|
|
- **Historique** : la réponse redeem n'echoait pas `argon2` → params par
|
|
défaut (correct aujourd'hui, lockout latent si params non-défaut).
|
|
|
|
### E5 · la clé du coffre n'est plus en clair dans le web storage
|
|
- **ACTION** : coffre déverrouillé → console WebView2 :
|
|
`sessionStorage.getItem('cryptoKey')`.
|
|
- **ATTENDU** : `null`. (Avant : une string base64 de 44 car = la clé AES.)
|
|
- **Bonus** : dump mémoire du renderer → `this_is_password` peut persister
|
|
(strings V8, non corrigeable) ; c'est la **clé** qu'on a retirée du storage.
|
|
|
|
### E6 · non-régression — les 4 exporteurs de clé marchent toujours
|
|
Après le retrait du cache sessionStorage, re-tester que ces flux (qui
|
|
`exportKey` la clé) fonctionnent : **login**, **Quick Unlock**, **PIN**,
|
|
**recovery redeem**, **change master password**.
|
|
|
|
### E7 · non-régression — éditeur slideover après suppression du modal mort
|
|
Le générateur, les tags et la barre de force étaient partagés. Vérifier dans
|
|
l'éditeur slideover : créer + éditer une entrée, ajouter/retirer des tags,
|
|
**Generate → Use** insère le mot de passe, la barre de force réagit.
|
|
|
|
---
|
|
|
|
## Reste à traiter (constaté pendant l'audit, non corrigé)
|
|
|
|
1. **Attachments hors garde A11.** Le contrôle « changed elsewhere » couvre le
|
|
payload de l'entrée, pas ses pièces jointes (table séparée, écrites par
|
|
leurs propres endpoints). Un sync qui remplace une pièce jointe pendant
|
|
l'édition n'est pas signalé. Faible risque : l'éditeur ne re-ship jamais
|
|
les pièces jointes existantes, donc il n'y a rien à écraser.
|