libra/tests
Padreug cf1a0967bf fix(settlement): per-currency netting, balance guards, Decimal rates
Settlement correctness cluster from CODE-REVIEW-2026-06 (#2, #8, #13)
plus libra-#38:

- format_net_settlement_entry now enforces the same inline balance
  constraint as the fiat formatter (payment = receivable - payable
  + credit) and grows an optional credit leg. An unbalanced
  settlement raises instead of reaching the ledger.
- on_invoice_paid settles only what the payment covers: a partial
  payment clears that much receivable; excess (or a payment with
  nothing owed) becomes user credit. Previously the full prior
  balance was cleared against a smaller payment, shipping unbalanced
  postings. Settlement links are attached only when the payment
  clears the full open balance, and only for same-currency entries.
- get_unsettled_entries_bql returns each entry's real posting
  currency (was hardcoded "EUR") and exact Decimal amount strings
  (was float). /receivables/settle nets only entries denominated in
  the settlement currency.
- fiat_rate/btc_rate metadata computed via Decimal (new
  fiat_rate_metadata helper) instead of float division — cost-basis
  records no longer carry float drift.
- format_posting_at_average_cost omits the cost braces when
  cost_currency is unset ("SATS {}" is invalid Beancount).
- Underpay error payload serializes amounts as exact Decimal strings.
- validate_metadata catches decimal.InvalidOperation so bad
  fiat_amount input becomes ValidationError (libra-#38); flipped the
  tracking xfail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:54:46 +02:00
..
__init__.py Add integration test suite 2026-06-07 15:39:45 +02:00
conftest.py test(harness): split-layout ledger + disable rate limiter 2026-06-16 23:25:27 +02:00
helpers.py test(accounts): cover admin add-account endpoint 2026-06-16 23:25:27 +02:00
README.md docs(tests): record known-good lnbits/dev invocation + env gotchas 2026-06-12 20:39:14 +02:00
test_admin_chart_accounts_api.py fix(accounts): recover ledger-only account instead of blanket 409 (libra-#50) 2026-06-17 10:13:43 +02:00
test_balances_api.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_entries_admin_api.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_entries_user_api.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_entry_identity_api.py Resolve entry identity via entry-id metadata; unfuse user references (libra-#42) 2026-06-12 20:39:06 +02:00
test_lightning_api.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_manual_payment_requests_api.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_migrations.py fix(payments): add local idempotency gate for Lightning recording 2026-07-12 12:33:42 +02:00
test_payment_idempotency.py fix(payments): record-payment fails closed and shares the claim gate 2026-07-12 12:41:42 +02:00
test_reconciliation_api.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_settings_auth_api.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_settlement_api.py fix(settlement): per-currency netting, balance guards, Decimal rates 2026-07-12 12:54:46 +02:00
test_smoke.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_unit.py fix(settlement): per-currency netting, balance guards, Decimal rates 2026-07-12 12:54:46 +02:00
test_void_reject_api.py Add integration test suite 2026-06-07 15:39:45 +02:00

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_PATH is the parent of an extensions/ 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/extensions makes libra invisible: zero extensions install, migrations never run, and every test errors with no such table: extension_settings.
  • Set LNBITS_DATA_FOLDER to a fresh temp dir explicitly. The conftest's os.environ.setdefault redirect is not always effective; reusing a previous run's database fails first_install with "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 main does not work: extensions importing lnbits.core.signers fail 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.py for 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.approx for Decimal/float tolerance.
  • 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_b fixtures. The ledger is session-shared and accumulates entries; test isolation comes from unique user IDs, not ledger resets.