Add integration test suite

113 passing tests + 3 skipped + 8 xfailed across 10 files, covering user
expense and income flow, admin receivable/revenue, settings + auth gates,
void/reject, manual payment requests, balance display, Lightning auth
paths, reconciliation API, and pure-function units. Runs against a real
Fava subprocess and full LNbits app via asgi_lifespan; the harness
captures the auth-flow / settings / env-var disciplines surfaced during
build-out (see tests/README.md and tests/conftest.py docstring).

Eight xfailed/skipped tests carry full implementations gated behind issues
#38, #39, #40 — they flip back on automatically when those land.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-07 09:47:09 +02:00
commit 7a4b3022c2
14 changed files with 3710 additions and 0 deletions

View file

@ -0,0 +1,219 @@
"""Admin-side journal entry endpoints — receivable and revenue.
- `POST /libra/api/v1/entries/receivable` admin records that a user owes
libra. Lands as a pending (`!`) entry, balance untouched until approve.
- `POST /libra/api/v1/entries/revenue` admin records that libra received
a payment unrelated to any user. Lands as a cleared (`*`) entry, no
approval needed.
Auth gate covered too: a regular user's wallet admin-key passes
`require_admin_key` but fails the super-user identity check in libra's own
`require_super_user`, so the endpoint returns 403.
"""
from uuid import uuid4
import pytest
from .helpers import (
get_balance,
list_user_entries,
post_receivable,
post_revenue,
)
@pytest.mark.anyio
async def test_admin_records_receivable_lands_cleared(
client, super_user_headers, configured_user, standard_accounts,
):
"""Admin posts a receivable for a user — the Beancount entry is written
with the cleared `*` flag immediately (not pending). The user's balance
reflects the debt without an approve step.
Note: `JournalEntry.flag` in the API response is misleading it's a
leftover of the legacy model and reports PENDING, but the entry in
Beancount is written as `*`. The on-disk reality is what affects the
balance, so that's what we assert.
"""
user, wallet = configured_user
response = await post_receivable(
client,
super_user_headers=super_user_headers,
user_id=user.id,
amount="200.00",
currency="EUR",
description=f"December rent share {uuid4().hex[:6]}",
revenue_account=standard_accounts["revenue_rent"]["name"],
)
assert response.get("id"), f"expected id in response, got {response}"
# Force a fresh Fava read before checking balance — Fava lazily reloads
# the .beancount file and a balance call right after add_entry can hit
# a stale view.
await list_user_entries(client, wallet_inkey=wallet.inkey)
balance = await get_balance(client, wallet_inkey=wallet.inkey)
eur = balance.get("fiat_balances", {}).get("EUR")
assert eur is not None, f"expected EUR in fiat_balances, got {balance}"
assert float(eur) == pytest.approx(200.0), (
f"expected +200 EUR (user-owes-libra) after receivable, got {eur}"
)
@pytest.mark.anyio
async def test_receivable_visible_in_target_users_journal(
client, super_user_headers, configured_user, standard_accounts,
):
"""The receivable shows up in the *debtor* user's journal listing
(not just in the admin view)."""
user, wallet = configured_user
tag = uuid4().hex[:6]
await post_receivable(
client,
super_user_headers=super_user_headers,
user_id=user.id,
amount="75.00",
currency="EUR",
description=f"Workshop fee {tag}",
revenue_account=standard_accounts["revenue_fees"]["name"],
)
listing = await list_user_entries(client, wallet_inkey=wallet.inkey)
descriptions = [e.get("description") or "" for e in listing.get("entries", [])]
assert any(tag in d for d in descriptions), (
f"receivable missing from debtor's journal: {descriptions}"
)
@pytest.mark.anyio
async def test_admin_records_revenue_clears_immediately(
client, super_user_headers, standard_accounts,
):
"""Revenue (libra received money, no user debt) is cleared on creation —
no admin approval step."""
response = await post_revenue(
client,
super_user_headers=super_user_headers,
amount="500.00",
currency="EUR",
description=f"Workshop fees collected {uuid4().hex[:6]}",
revenue_account=standard_accounts["revenue_fees"]["name"],
payment_method_account="Assets:Cash",
)
assert response.get("id"), f"expected id in response, got {response}"
# Cleared on creation — flag is `*`, no approve_entry call needed.
@pytest.mark.anyio
async def test_non_super_user_cannot_post_receivable(
client, configured_user, standard_accounts,
):
"""A regular user's wallet admin key passes `require_admin_key` but
fails libra's super-user identity check. Returns 403."""
user, wallet = configured_user
admin_key_headers = {"X-Api-Key": wallet.adminkey, "Content-type": "application/json"}
r = await client.post(
"/libra/api/v1/entries/receivable",
headers=admin_key_headers,
json={
"user_id": user.id,
"amount": "10.00",
"currency": "EUR",
"description": "Should be denied",
"revenue_account": standard_accounts["revenue_rent"]["name"],
},
)
assert r.status_code == 403, f"expected 403, got {r.status_code}: {r.text}"
assert "super" in r.text.lower(), (
f"expected super-user error message, got {r.text!r}"
)
@pytest.mark.anyio
async def test_non_super_user_cannot_post_revenue(
client, configured_user, standard_accounts,
):
"""Same super-user gate covers the revenue endpoint."""
_, wallet = configured_user
admin_key_headers = {"X-Api-Key": wallet.adminkey, "Content-type": "application/json"}
r = await client.post(
"/libra/api/v1/entries/revenue",
headers=admin_key_headers,
json={
"amount": "10.00",
"currency": "EUR",
"description": "Should be denied",
"revenue_account": standard_accounts["revenue_fees"]["name"],
"payment_method_account": "Assets:Cash",
},
)
assert r.status_code == 403, f"expected 403, got {r.status_code}: {r.text}"
assert "super" in r.text.lower()
@pytest.mark.anyio
async def test_receivable_unknown_revenue_account_returns_404(
client, super_user_headers, configured_user,
):
"""An admin posting against a non-existent revenue account gets 404."""
user, _ = configured_user
r = await client.post(
"/libra/api/v1/entries/receivable",
headers=super_user_headers,
json={
"user_id": user.id,
"amount": "10.00",
"currency": "EUR",
"description": "Bad account",
"revenue_account": f"Income:Test:DoesNotExist-{uuid4().hex[:6]}",
},
)
assert r.status_code == 404, f"expected 404, got {r.status_code}: {r.text}"
assert "not found" in r.text.lower()
@pytest.mark.anyio
async def test_receivable_unknown_currency_returns_400(
client, super_user_headers, configured_user, standard_accounts,
):
"""Currency validation hits before account lookups."""
user, _ = configured_user
r = await client.post(
"/libra/api/v1/entries/receivable",
headers=super_user_headers,
json={
"user_id": user.id,
"amount": "10.00",
"currency": "XYZ",
"description": "Bogus currency",
"revenue_account": standard_accounts["revenue_rent"]["name"],
},
)
assert r.status_code == 400, f"expected 400, got {r.status_code}: {r.text}"
assert "currency" in r.text.lower() or "xyz" in r.text.lower()
@pytest.mark.anyio
async def test_revenue_unknown_payment_account_returns_404(
client, super_user_headers, standard_accounts,
):
"""Revenue endpoint validates BOTH accounts; the payment-method one too."""
r = await client.post(
"/libra/api/v1/entries/revenue",
headers=super_user_headers,
json={
"amount": "10.00",
"currency": "EUR",
"description": "Bad payment account",
"revenue_account": standard_accounts["revenue_fees"]["name"],
"payment_method_account": f"Assets:DoesNotExist-{uuid4().hex[:6]}",
},
)
assert r.status_code == 404, f"expected 404, got {r.status_code}: {r.text}"
assert "not found" in r.text.lower()