Same discriminant as the Android client - "/api/v1/repos/" in the URL - so one address tells both ends which transport to speak, and nothing new appears in Settings beyond a placeholder and a warning. The bridge stays shared. Three things differ, and they are confined: the header is "token <PAT>" rather than Basic (Gitea answers a silent 401 to the other), a PUT carries a JSON envelope with the file base64-encoded inside it, and the connection test uses GET because the contents API does not answer HEAD. The sha does the ETag's whole job. It comes back from the pull, rides in the PUT body instead of an If-Match, and a stale one earns a 409 - handled exactly like WebDAV's 412, by re-running pull->merge->push so the other device's changes are folded in rather than clobbered. On a first push `sha` is OMITTED rather than sent empty: Gitea reads its presence as "replace that exact version". `branch` is omitted too, so the repo's default is used instead of a name we would be guessing. A failing PUT now reads its response body: Gitea explains a 409 or a 422 there, and without it the error was just a number. The test button checks the SHAPE of what came back, not only the status. A URL one path segment off answers 200 with valid JSON that is not a file - which would otherwise surface much later as "wrong sync password" and send you looking on the wrong side. Four tests cover that parsing, including the line-wrapped base64 Gitea sends and the array an accidental directory URL returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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/letdon't attach to the vm global. Top-levelfunction/vardeclarations become properties of the context global, butconst 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.deepStrictEqualtrips on the prototype check. Structural comparisons normalize through JSON first (seeeqDeepincsv.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.