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:
parent
9c88993c13
commit
7a4b3022c2
14 changed files with 3710 additions and 0 deletions
219
tests/test_entries_admin_api.py
Normal file
219
tests/test_entries_admin_api.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue