Files
Password-Manager/js/tests/README.md
T
r-zakarya d9397881dc test: add frontend unit suite + fix mixed local/UTC timestamps
Two CODE_AUDIT items in one session.

§3.2 — Frontend regression net (js/tests/, 35 tests, node:test, zero deps):
- harness.js loads app.js (monofile, no exports) into a node:vm with browser
  globals stubbed, surfacing internals via an export epilogue.
- crypto: deriveKeyAndVerifier (AES key == raw PBKDF2, cross-checked vs Node
  pbkdf2Sync), legacy-vs-v2 verifier decoupling, encrypt/decrypt round-trip,
  IV uniqueness, AEAD tamper/wrong-key.
- csv: parseCSV tokenizer, findColumn heuristics, Bitwarden/KeePass mapping.
- merge: applyRemoteSnapshot add/update/skip (LWW), tombstone delete,
  resurrection arbitration (both NaN branches), local-tombstone veto,
  additive folder merge. Only api() is stubbed; loadEntries/encryptImportEntry
  run for real.
- Wired as a build gate in BuildAssets.ps1 (after node --check, bypass
  PM_SKIP_TESTS=1).

§2.2 — Unify timestamps on UTC:
- Entry created_at/updated_at were written via Delphi FormatDateTime(Now)
  = LOCAL, while deleted_at/tombstones use SQLite CURRENT_TIMESTAMP = UTC.
  The tombstone-resurrection arbitration compared the two zones, skewing by
  the machine's UTC offset even single-device.
- Add NowUTC/NowUTCStr to PM.Database, swap in at every entry/attachment
  write site (Entries create/update/bulk, Attachments POST echo).
- No JS change needed: arbitration now compares same-zone values.
- Existing rows self-heal on next edit (no destructive migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:17:33 +01:00

59 lines
2.8 KiB
Markdown

# 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.