# Frontend unit tests Regression net for the highest-risk pure/near-pure logic in `js/app.js`: crypto round-trip, KDF/verifier derivation, CSV import parsing, and the sync-merge / tombstone-resurrection arbitration. Addresses `CODE_AUDIT.md` §3.2 (aucun test automatisé). ## Running ``` npm test # or directly: node --test "js/tests/**/*.test.js" ``` Zero dependencies — uses the Node built-in test runner (`node:test`) and `webcrypto`. Requires Node ≥ 18 (developed on v24). Runs in ~1.7 s. ## How it works — `harness.js` `app.js` is a ~12k-line browser monofile with **no module exports** and one top-level side effect (a `DOMContentLoaded` listener). The harness loads the file's source into a `node:vm` context with browser globals stubbed (`crypto`, `localStorage`, `document`, `location`, …) so `init()` never fires, then appends an export epilogue that surfaces the internals on `globalThis.__test`. Two gotchas the harness works around, both documented inline: - **`const`/`let` don't attach to the vm global.** Top-level `function`/`var` declarations become properties of the context global, but `const state`, `const HASH_ALGO_V2`, etc. do not — hence the explicit export epilogue. - **Cross-realm prototypes.** Values returned from the sandbox carry the sandbox realm's prototypes, so `assert.deepStrictEqual` trips on the prototype check. Structural comparisons normalize through JSON first (see `eqDeep` in `csv.test.js`). The merge tests stub only the `api()` seam (a `function` declaration → overridable global property) with an in-memory fake server; `loadEntries`, `loadFolders`, and `encryptImportEntry` all run for real against it — so the tests exercise the actual pull→merge path, not a re-implementation. ## Suites | File | Covers | |---|---| | `crypto.test.js` | `deriveKeyAndVerifier` (AES key == raw PBKDF2, cross-checked vs Node's `pbkdf2Sync`), legacy vs `-v2` verifier decoupling, `encryptPwd`/`decryptPwd` round-trip, IV uniqueness, AEAD tamper/wrong-key → `[ERROR]` | | `csv.test.js` | `parseCSV` tokenizer (quotes, escaped `""`, CRLF, trailing field), `findColumn` header heuristics, `parseEntriesFromCSV` for Bitwarden/KeePass shapes, note-vs-login classification | | `merge.test.js` | `applyRemoteSnapshot`: add/update/skip (last-write-wins), tombstone delete, resurrection arbitration (both `NaN` branches), local-tombstone veto, additive folder merge | ## Notes on encoded behavior `merge.test.js` locks in one deliberately-asymmetric behavior: an unparseable **local** `updated_at` favours KEEP (resurrection wins), but an unparseable **remote** `deleted_at` still applies the delete. In production `deleted_at` is always server-formatted (parseable), so this only matters for a corrupted remote snapshot. If that arbitration is ever changed, the two `unparseable …` tests are where to update the expectation.