Files
Password-Manager/js/tests
r-zakarya dd86b2bd23 perf(crypto): derive Argon2id via argon2idAsync (unfreeze unlock UI)
deriveKeyBytes now calls NobleArgon2.argon2idAsync instead of the sync
argon2id, so it yields to the event loop periodically and the busy/unlock
spinner keeps animating instead of freezing ~0.65 s during login, register,
and master-pw rotation. Same result (both RFC-9106-verified); all callers
already await deriveKeyBytes so no call-site changes.

- Re-vendored js/argon2.js to export argon2idAsync alongside argon2id
  (re-bundled from @noble/hashes@2.2.0; both variants pass the RFC 9106 §5.3
  vector). 27KB → 29KB.
- Added a sync/async parity test. 63/63 green.
- Closes the last open item of CODE_AUDIT §1.2.

NOTE: argon2.js grew — run BuildAssets to re-embed it before the next
Delphi build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:41:59 +01:00
..

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

The frontend is a large browser script with no module exports and one top-level side effect (a DOMContentLoaded listener). It's being split into ordered classic-script files (§3.1); the harness concatenates the app parts in load order (APP_PARTS = app.crypto.js + app.js) into one source — node:vm doesn't share top-level const/let across separate runInContext calls the way the browser shares them across <script> tags. argon2.js is a self-contained IIFE and loads separately first. The concatenated source runs in a node:vm context with browser globals stubbed (crypto, localStorage, document, location, …) so init() never fires, then an export epilogue 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.