Fava-client hardening cluster (CODE-REVIEW-2026-06 #7, #14, #15, #19 + libra-#23, libra-#53): - New FavaClient.transform_source_line does the whole read-checksum-modify-write under the global write lock and maps Fava 409/412 to ChecksumConflictError. The approve and reject endpoints used to do this dance with raw httpx and no lock — two concurrent mutations raced each other and every other ledger writer (libra-#23). They now route through the new method and translate conflicts to HTTP 409. - update_entry_source / delete_entry raise ChecksumConflictError on 409/412 instead of leaking raw HTTPStatusError. - One shared httpx.AsyncClient per FavaClient (12 per-call instantiations removed — no more TCP handshake per request); closed via libra_stop. Health probes keep their 2s timeout per-request. - Account names/patterns are validated against ^[A-Za-z0-9:_-]+$ before interpolation into BQL string literals. - The posting amount regexes are consolidated into module-level compiled patterns, all decimal-tolerant — the old integer-only SATS pattern silently dropped decimal-SATS postings (Fava's @@->@ normalisation emits them) from balances. - add-account no longer verifies its own write with a second serialized get_all_accounts round-trip (libra-#53): sync_single_account_from_beancount grows an assume_exists path. New test: concurrent approve+reject must both land (was lost-update/412 before the lock). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| __init__.py | ||
| conftest.py | ||
| helpers.py | ||
| README.md | ||
| test_admin_chart_accounts_api.py | ||
| test_balances_api.py | ||
| test_entries_admin_api.py | ||
| test_entries_user_api.py | ||
| test_entry_identity_api.py | ||
| test_lightning_api.py | ||
| test_manual_payment_requests_api.py | ||
| test_migrations.py | ||
| test_payment_idempotency.py | ||
| test_reconciliation_api.py | ||
| test_settings_auth_api.py | ||
| test_settlement_api.py | ||
| test_smoke.py | ||
| test_unit.py | ||
| test_void_reject_api.py | ||
Libra extension tests
Integration tests covering the user- and admin-facing flows of the libra extension. Tests run against a real fava subprocess and a full LNbits app so they catch behaviour that mocks would miss (BQL semantics, Beancount arithmetic, multi-currency aggregation, HTTP boundary).
Layout
conftest.py— session-scoped Fava subprocess + LNbits app + user/wallet fixtures.helpers.py— high-level wrappers for the common API flows (post_expense,settle_receivable,approve_manual_payment_request, …). One per intention, so test bodies read as sequences of actions rather than HTTP calls.test_smoke.py— single end-to-end test; run first to validate the harness.test_<area>_api.py— per-flow coverage (entries, balances, settlement, manual payment requests, lightning, reconciliation, settings/auth, void/reject).test_unit.py— pure functions (beancount_format,account_utils,core/validation); no harness.
Prerequisites
The harness requires fava on PATH. On NixOS:
nix-shell -p python3Packages.fava
Inside the regtest container fava is already provisioned.
Running
The suite targets the lnbits/dev worktree (~/dev/lnbits/dev) — it
relies on dev-branch modules (lnbits.core.signers, the bunker work) that
main doesn't carry. A known-good invocation from scratch:
# One-time: build a venv with lnbits (dev) + test deps + fava
nix-shell -p uv --run "uv venv /tmp/libra-test-venv --python 3.12 && \
uv pip install --python /tmp/libra-test-venv/bin/python \
-e ~/dev/lnbits/dev pytest asgi-lifespan fava"
# Run (each invocation gets a fresh data folder — REQUIRED, see gotchas)
cd ~/dev/lnbits/dev && \
env LNBITS_KEY_MASTER=$(openssl rand -hex 32) \
LNBITS_DATA_FOLDER=$(mktemp -d -t libra-test-data-XXXX) \
LNBITS_EXTENSIONS_PATH=$HOME/dev/shared \
PYTHONPATH=$HOME/dev/shared/extensions:. \
PATH=/tmp/libra-test-venv/bin:$PATH \
/tmp/libra-test-venv/bin/pytest ~/dev/shared/extensions/libra/tests -q
# Smoke test only (validate the harness before running everything)
... pytest path/to/libra/tests/test_smoke.py
# One area
... pytest path/to/libra/tests/test_balances_api.py
# Single test, verbose
... pytest path/to/libra/tests/test_balances_api.py::test_mixed_income_expense_nets_correctly -v
Environment gotchas (each cost a failed run on 2026-06-12)
LNBITS_EXTENSIONS_PATHis the parent of anextensions/dir — lnbits scans{path}/extensions/(lnbits/app.py,build_all_installed_extensions_list). For extensions at~/dev/shared/extensions/libra, pass~/dev/shared. Pointing it at~/dev/shared/extensionsmakes libra invisible: zero extensions install, migrations never run, and every test errors withno such table: extension_settings.- Set
LNBITS_DATA_FOLDERto a fresh temp dir explicitly. The conftest'sos.environ.setdefaultredirect is not always effective; reusing a previous run's database failsfirst_installwith "Username already exists" during app-fixture setup. LNBITS_KEY_MASTER(32-byte hex) is mandatory on lnbits dev — the signer migration aborts startup without it (issue lnbits#9 encrypt-at-rest). Any random value is fine for tests.- lnbits
maindoes not work: extensions importinglnbits.core.signersfail to load, and libra's app fixture errors.
The Fava subprocess starts once per session (~1-2s) and is shared across tests; each test creates its own LNbits user so the shared ledger doesn't cause inter-test interference.
Conventions
- Tests assert intent, not shape. Use the helpers in
helpers.pyfor the request and assert on the meaning of the response (balance values, account names, settlement state), not on incidental keys in the JSON. This keeps tests resilient to non-behavioural API tweaks. - Currency-handling assertions use
pytest.approxforDecimal/floattolerance. - One canonical happy path per flow, plus boundary cases that matter (voided entries excluded, pending entries excluded, cross-user isolation, auth gate rejection). Don't over-matrix.
- Each test creates its own users via the function-scoped
libra_user/libra_user_bfixtures. The ledger is session-shared and accumulates entries; test isolation comes from unique user IDs, not ledger resets.