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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user