diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index 859ae31..93dbd74 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -80,13 +80,16 @@ Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le ta 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 +- **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 +### 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. +- **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** — surtout pas « Saved ». +- **Recette locale, sans second appareil** : ouvrir `A` et la rendre dirty → dans DB Browser, app lancée, `DELETE FROM vault_entries WHERE id = ` puis **Write Changes** (sans ça le WAL ne publie rien) → dans l'app, déclencher un `loadEntries()` qui ne concerne pas `A` (mettre une **autre** entrée à la corbeille puis *Empty trash*) → Save. +- **Bug trouvé par ce test** : le serveur répondait **200 « Updated »** pour un `UPDATE` n'ayant touché **aucune** ligne — le client affichait « Saved » alors que rien n'était écrit. Perte de données silencieuse. `HandleUpdateEntry` teste maintenant `RowsAffected` et renvoie 404. +- **Même classe, non corrigé** : delete / restore / favorite / pin / set-icon répondent aussi OK sur un id inexistant. Assumé — aucun ne prétend avoir enregistré un travail de l'utilisateur, et le client recharge la liste juste après. --- diff --git a/delphi-backend/Handlers/PM.Handler.Entries.pas b/delphi-backend/Handlers/PM.Handler.Entries.pas index eb97652..c55c8f0 100644 --- a/delphi-backend/Handlers/PM.Handler.Entries.pas +++ b/delphi-backend/Handlers/PM.Handler.Entries.pas @@ -514,6 +514,7 @@ var LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string; LHasTemplate: Boolean; LQ: TFDQuery; + LRows: Integer; begin try LUserId := Authenticate(ARequest, AResponse); @@ -664,6 +665,7 @@ begin LQ.ParamByName('id').AsInteger := LId; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ExecSQL; + LRows := LQ.RowsAffected; finally LQ.Free; end; @@ -671,6 +673,15 @@ begin DB.Unlock; end; + // No row matched: the entry was deleted (another device's sync, a purge) + // between the editor opening it and this save. Reporting 200 here made the + // client show "Saved" for an edit that went nowhere — silent data loss. + if LRows <= 0 then + begin + TJSONHelper.SendError(AResponse, 404, 'Entry no longer exists'); + Exit; + end; + LogAudit(LUserId, 'edit_entry', GetClientIP(ARequest)); TJSONHelper.SendOK(AResponse, 'Updated'); end;