diff --git a/tests/conftest.py b/tests/conftest.py index 44b5c26..6698018 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -108,9 +108,6 @@ def _settings_cleanup(settings: Settings) -> None: settings.lnbits_user_activation_by_invitation_code = False settings.lnbits_register_reusable_activation_code = "" settings.lnbits_register_one_time_activation_codes = [] - # Keep the rate limiter disabled across per-test settings resets (the - # limiter itself is fixed at app-creation time, but keep the value coherent). - settings.lnbits_rate_limit_no = 1_000_000 @pytest.fixture(scope="session") @@ -136,12 +133,6 @@ def settings() -> Iterator[Settings]: lnbits_settings.lnbits_admin_ui = True lnbits_settings.lnbits_extensions_default_install = [] lnbits_settings.lnbits_extensions_deactivate_all = False - # The full suite fires >200 requests/minute; the default rate limit (200/min) - # otherwise 429s fixture setup intermittently. The limiter is built once at - # app creation from this value (lnbits/app.py register_new_ratelimiter), and - # this fixture runs before the `app` fixture, so raising it here disables it - # for the session. - lnbits_settings.lnbits_rate_limit_no = 1_000_000 yield lnbits_settings @@ -179,32 +170,13 @@ option "render_commas" "TRUE" 2020-01-01 open Equity:Opening-Balances EUR,SATS 2020-01-01 open Income:Generic EUR,SATS 2020-01-01 open Expenses:Generic EUR,SATS - -include "accounts/chart.beancount" -include "accounts/users.beancount" """ -# Split-layout include targets, mirroring the production fava layout -# (aiolabs/server-deploy#4). libra's fava_client routes Open directives by -# account name (fava_client._infer_target_file): per-user accounts -# (:User-xxxxxxxx) to accounts/users.beancount, everything else to -# accounts/chart.beancount. Both must exist as Fava *source* files (i.e. be -# included) or /api/source writes 500 with "non-source file". The title stays -# in the root ledger above so Fava's slug still matches LEDGER_SLUG (scalar -# options don't propagate from includes — see aiolabs/server-deploy#9). -CHART_SEED = "; Admin-mutable chart of accounts (libra appends Open directives).\n" -USERS_SEED = "; Per-user account opens (libra appends at signup).\n" - @pytest.fixture(scope="session") def fava_ledger_path(tmp_path_factory: pytest.TempPathFactory) -> Path: - """Session-scoped split ledger Fava reads from: a root file that includes - accounts/chart.beancount (admin add-account target) and - accounts/users.beancount (per-user opens target).""" + """Session-scoped .beancount file Fava reads from.""" ledger_dir = tmp_path_factory.mktemp("libra-ledger") - (ledger_dir / "accounts").mkdir() - (ledger_dir / "accounts" / "chart.beancount").write_text(CHART_SEED) - (ledger_dir / "accounts" / "users.beancount").write_text(USERS_SEED) ledger = ledger_dir / f"{LEDGER_SLUG}.beancount" ledger.write_text(MINIMAL_LEDGER) return ledger diff --git a/tests/helpers.py b/tests/helpers.py index 02d8f78..80ad343 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -13,7 +13,7 @@ separate ISO code field — this matches `models.ExpenseEntry` / `ReceivableEntr from decimal import Decimal from typing import Any, Optional, Union -from httpx import AsyncClient, Response +from httpx import AsyncClient Amount = Union[Decimal, int, float, str] @@ -106,26 +106,6 @@ async def grant_permission( return r.json() -async def add_chart_account( - client: AsyncClient, - *, - super_user_headers: dict, - name: str, - description: Optional[str] = None, -) -> Response: - """Super user adds a chart-of-accounts entry via the admin endpoint - (POST /api/v1/admin/accounts). Returns the raw Response so callers can - assert on status codes (201 / 400 / 409 / 403).""" - body: dict[str, Any] = {"name": name} - if description is not None: - body["description"] = description - return await client.post( - "/libra/api/v1/admin/accounts", - headers=super_user_headers, - json=body, - ) - - # --------------------------------------------------------------------------- # Entries — user side # --------------------------------------------------------------------------- diff --git a/tests/test_admin_chart_accounts_api.py b/tests/test_admin_chart_accounts_api.py deleted file mode 100644 index 1f574f9..0000000 --- a/tests/test_admin_chart_accounts_api.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Admin chart-of-accounts endpoint — POST /api/v1/admin/accounts. - -Covers the endpoint wired into the UI's "Add Account" dialog: - - - Writes an Open directive to accounts/chart.beancount via Fava /api/source, - *unconstrained* by currency (the directive needs no currency list), with - provenance + description metadata (escaped for Beancount). - - Mirrors the account into libra's DB (synced_to_libra_db). - - Rejects duplicates with 409, malformed names with 400, and non-super-users - with 403. - -The harness ledger is the split layout (root includes accounts/chart.beancount) -so the endpoint's hardcoded target_file resolves — see conftest.CHART_SEED. -""" -import re -from pathlib import Path -from uuid import uuid4 - -import pytest - -from .helpers import add_chart_account - - -def _chart_text(fava_ledger_path: Path) -> str: - return (fava_ledger_path.parent / "accounts" / "chart.beancount").read_text() - - -def _unique(prefix: str = "Expenses:Test") -> str: - # Capitalized leaf (valid Beancount component) unique per call so the - # session-scoped ledger doesn't collide across tests. - return f"{prefix}:T{uuid4().hex[:8].upper()}" - - -@pytest.mark.anyio -async def test_add_chart_account_writes_unconstrained_open_with_escaped_meta( - client, super_user_headers, fava_ledger_path, -): - """Happy path: 201, the Open directive carries no currency constraint, the - description metadata is escaped, and the account is synced into libra's DB.""" - name = _unique() - r = await add_chart_account( - client, - super_user_headers=super_user_headers, - name=name, - description='has a "quote" and ok', - ) - assert r.status_code == 201, f"expected 201, got {r.status_code}: {r.text}" - body = r.json() - assert body["account_name"] == name - assert body["synced_to_libra_db"] is True - - chart = _chart_text(fava_ledger_path) - # Open present and UNCONSTRAINED: the account name is followed directly by - # end-of-line, not " EUR, SATS, USD". - assert re.search(rf"^\d{{4}}-\d{{2}}-\d{{2}} open {re.escape(name)}$", chart, re.MULTILINE), ( - f"expected an unconstrained Open for {name}, chart was:\n{chart}" - ) - # Description metadata is escaped so the quote can't break the ledger. - assert r'description: "has a \"quote\" and ok"' in chart - assert 'source: "admin-ui"' in chart - - -@pytest.mark.anyio -async def test_add_chart_account_with_explicit_currencies_constrains_open( - client, super_user_headers, fava_ledger_path, -): - """API callers may still pass an explicit currency constraint (the UI never - does). When provided, it lands on the Open directive.""" - name = _unique() - r = await client.post( - "/libra/api/v1/admin/accounts", - headers=super_user_headers, - json={"name": name, "currencies": ["EUR", "SATS"]}, - ) - assert r.status_code == 201, f"expected 201, got {r.status_code}: {r.text}" - chart = _chart_text(fava_ledger_path) - assert re.search(rf"open {re.escape(name)} EUR, SATS$", chart, re.MULTILINE), ( - f"expected a currency-constrained Open for {name}, chart was:\n{chart}" - ) - - -@pytest.mark.anyio -async def test_add_chart_account_duplicate_returns_409( - client, super_user_headers, -): - """Adding the same account twice: first 201, second 409 (not a false success).""" - name = _unique() - first = await add_chart_account(client, super_user_headers=super_user_headers, name=name) - assert first.status_code == 201, f"first add: {first.status_code} {first.text}" - - second = await add_chart_account(client, super_user_headers=super_user_headers, name=name) - assert second.status_code == 409, f"expected 409, got {second.status_code}: {second.text}" - assert "already exists" in second.json().get("detail", "").lower() - - -@pytest.mark.anyio -async def test_add_chart_account_invalid_prefix_returns_400( - client, super_user_headers, fava_ledger_path, -): - """A root outside the five valid types is rejected and never written.""" - before = _chart_text(fava_ledger_path) - r = await add_chart_account(client, super_user_headers=super_user_headers, name="Foo:Bar") - assert r.status_code == 400, f"expected 400, got {r.status_code}: {r.text}" - assert _chart_text(fava_ledger_path) == before, "rejected account must not be written" - - -@pytest.mark.anyio -@pytest.mark.parametrize( - "bad_name", - [ - "Expenses:Foo Bar", # space - "Expenses:foo", # lowercase sub-component start - "Expenses:Foo!", # punctuation - "Expenses:", # no sub-account - "Expenses:Foo::Bar", # empty component - ], -) -async def test_add_chart_account_invalid_characters_returns_400( - client, super_user_headers, fava_ledger_path, bad_name, -): - """Malformed account names are rejected server-side (the UI guard can be - bypassed via the API) and never reach the ledger.""" - before = _chart_text(fava_ledger_path) - r = await add_chart_account(client, super_user_headers=super_user_headers, name=bad_name) - assert r.status_code == 400, f"expected 400 for {bad_name!r}, got {r.status_code}: {r.text}" - assert _chart_text(fava_ledger_path) == before, "rejected account must not be written" - - -@pytest.mark.anyio -async def test_add_chart_account_requires_super_user( - client, configured_user, fava_ledger_path, -): - """A regular user's wallet admin-key passes require_admin_key but fails the - super-user identity check → 403, nothing written.""" - _user, wallet = configured_user - name = _unique() - before = _chart_text(fava_ledger_path) - r = await client.post( - "/libra/api/v1/admin/accounts", - headers={"X-Api-Key": wallet.adminkey, "Content-type": "application/json"}, - json={"name": name}, - ) - assert r.status_code == 403, f"expected 403, got {r.status_code}: {r.text}" - assert _chart_text(fava_ledger_path) == before, "unauthorized add must not be written"