Files
Password-Manager/TEST_SCENARIOS.md
T
2026-07-25 16:44:06 +01:00

18 KiB

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).✅

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.

B14 · dialogues natifs ouverts par un clic programmatique

  • ACTION : A ouverte + dirty → (a) Settings → Import vault ; (b) télécharger une pièce jointe (fallback web) ; (c) Upload icon ; (d) joindre un fichier.
  • ATTENDU : seul le dialogue natif s'ouvre. Pas de « Discard unsaved changes? » par-dessus, l'éditeur reste ouvert et modifié.
  • Historique : le <input type=file> (et le <a download>) est attaché à <body> puis .click() → le clic synthétique remontait au handler click-outside qui le prenait pour un clic dehors. Gardé par e.isTrusted.

F — Round-trip export / import (drapeaux & provenance)

F1 · favori + épingle survivent au restore

  • ACTION : épingler et mettre en favori une entrée → Export vault → supprimer l'entrée (corbeille + purge) → Import vault du fichier.
  • ATTENDU : l'entrée revient épinglée et favorite.
  • Historique : pinned n'était dans aucun container ; favorite y était écrit mais jamais relu (ni parser, ni body, ni INSERT) → les deux perdus au restore.

F2 · même chose depuis une auto-backup

  • ACTION : Backup now → supprimer l'entrée → importer le fichier vault-autobackup-*.json.
  • ATTENDU : identique à F1.

F3 · CSV étranger → drapeaux à 0

  • ACTION : importer un CSV sans colonnes favorite/pinned.
  • ATTENDU : entrées importées ni épinglées ni favorites (défaut 0), pas d'erreur.

F4 · le sync ne propage PAS les drapeaux (device-local, décidé 2026-07-11)

  • ACTION : épingler/favoriser une entrée sur le device A → Sync now → Sync now sur le device B.
  • ATTENDU : sur B, l'entrée n'est pas épinglée/favorite. Et un sync ne doit jamais retirer un drapeau posé localement sur B (le PUT serveur ne touche pas ces colonnes).

F5 · icône custom : provenance préservée

  • ACTION : upload d'une icône custom → export → supprimer l'entrée → import → puis Re-fetch all.
  • ATTENDU : l'icône custom revient et survit au Re-fetch all (le flag icon_custom a fait le round-trip).

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.


G — Vault health (série 2026-07-25)

G1 · bouton Fix quand le changement d'entrée est annulé

  • ACTION : A ouverte + dirty → aller sur Vault health → cliquer Fix sur une autre entrée B → Cancel au message discard.
  • ATTENDU : rien ne bouge. A reste ouverte, son mot de passe reste masqué, aucun clignotement du bouton Generate.
  • Puis refaire → Discard : B s'ouvre, c'est le mot de passe de B qui est démasqué + sélectionné, et c'est le Generate de B qui pulse.
  • Le focus RESTE sur le mot de passe (~1 s après le clic, vérifier que le curseur n'a pas sauté sur le titre) : openSlideOver programme un focus différé de 80 ms sur #soTitle, neutralisé ici par { noFocus: true }.
  • Historique : openSlideOver ne renvoyait rien, donc openEntryForFix continuait après une annulation et démasquait en clair le mot de passe de l'entrée restée ouverte.

G2 · Fix sur une entrée supprimée entre-temps

  • ACTION : ouvrir Vault health → dans un autre onglet de l'app, mettre à la corbeille une entrée listée → revenir sur health (sans Recompute) → Fix sur cette entrée.
  • ATTENDU : rien ne casse (l'éditeur ne s'ouvre pas, pas d'erreur JS). Avec G3 la liste devrait de toute façon être à jour.

G3 · le tableau de bord reflète les mutations

  • ACTION : ouvrir Vault health (noter le score) → revenir à la liste → faire une mutation : import, sync, suppression, batch delete, duplicate, restore, ou changement de master pw → rouvrir Vault health.
  • ATTENDU : score et listes recalculés (les entrées supprimées ont disparu, les importées apparaissent) sans cliquer Recompute.
  • Historique : le cache n'était vidé qu'au lock / au save / via Recompute — après un import ou une suppression le dashboard restait figé sur l'état précédent.

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.