libra/tests
Padreug 116df46d38 Net settlement + credit overflow on /receivables/settle (libra-#33, libra-#41)
When the caller omits settled_entry_links (the default), the endpoint
auto-detects open entries across both directions for the user and writes
a single transaction that:

  - Zeros every per-user account that has an open balance, not just the
    net (the libra-#33 bug — previously the 2-leg form left both Payable
    and Receivable carrying non-zero balances after a complete cash
    settlement, while only netting the cash side).
  - Routes any cash above the net obligation to Liabilities:Credit:User-X
    (libra-#41), so over-payment lands on a real liability account
    instead of silently drifting.
  - Attaches every reconciled source entry's link
    (exp-..., rcv-...) so a reader scanning the settlement transaction
    can trace what it cleared.

Cash less than the net obligation, with no explicit links, returns 400
with a structured diff (cash_paid, net_obligation, receivable_total,
payable_total). The operator either pays the exact net or passes
settled_entry_links to settle a specific subset; partial settlement
without a coherent target is not silently absorbed.

The legacy explicit-links code path is unchanged — callers that pass
settled_entry_links keep the 2-leg shape with no auto-detection. None
of the callers in libra or aiolabs/webapp currently use that field, but
the contract is preserved for the partial-settle-of-specific-entries
flow.

format_fiat_net_settlement_entry is the new helper for the 2/3/4-leg
shape; it enforces the cash-balance constraint inline so callers can't
accidentally produce an unbalanced transaction.

tests/test_settlement_api.py (6 tests) locks in:
  - Nancy's #33 scenario: receivable 100 + payable 50 + cash 50
    zeros both per-user accounts, links both source entries
  - Overpay: cash 70 against net 50 → credit balance 20
  - Pure receivable overpay → credit appears
  - Underpay without explicit links → 400 with diff
  - No open receivables → 400 with hint pointing at /payables/pay
  - Explicit settled_entry_links uses legacy 2-leg path

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 15:39:45 +02:00
..
__init__.py Add integration test suite 2026-06-07 15:39:45 +02:00
conftest.py Add integration test suite 2026-06-07 15:39:45 +02:00
helpers.py Add integration test suite 2026-06-07 15:39:45 +02:00
README.md Add integration test suite 2026-06-07 15:39:45 +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_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_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 Net settlement + credit overflow on /receivables/settle (libra-#33, libra-#41) 2026-06-07 15:39:45 +02:00
test_smoke.py Add integration test suite 2026-06-07 15:39:45 +02:00
test_unit.py Add integration test suite 2026-06-07 15:39:45 +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

From the LNbits source root (with the libra extension reachable via LNBITS_EXTENSIONS_PATH or symlinked into lnbits/extensions/):

# Whole suite
pytest path/to/libra/tests

# 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

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.