From 9e06fa0b2ad7f4eaf56092a718d3a87c62683997 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 12 Jul 2026 16:04:32 +0200 Subject: [PATCH] test(routes): snapshot the ordered route table before the package split FastAPI matches in registration order; the views_api split must keep the (methods, path, endpoint) table byte-identical. The snapshot also records the known wart it must NOT silently change: /accounts/hierarchy is registered after /accounts/{account_id} and therefore shadowed. Co-Authored-By: Claude Fable 5 --- tests/test_route_table.py | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/test_route_table.py diff --git a/tests/test_route_table.py b/tests/test_route_table.py new file mode 100644 index 0000000..4064975 --- /dev/null +++ b/tests/test_route_table.py @@ -0,0 +1,115 @@ +"""Route-table snapshot guard for the views_api package split. + +FastAPI matches routes in registration order, so a mechanical move of +endpoints between modules can silently change which endpoint answers a +path (literal vs {param} siblings). This snapshot pins the ordered +(methods, path, endpoint) table; the split must reproduce it exactly. + +Regenerate after a DELIBERATE routing change with: + + from libra.views_api import libra_api_router + for r in libra_api_router.routes: + print((",".join(sorted(r.methods)), r.path, r.endpoint.__name__)) + +Note the one known wart this table records: /api/v1/accounts/hierarchy +is registered AFTER /api/v1/accounts/{account_id}, so it is shadowed +(requests resolve as account_id="hierarchy"). Fixing that is a +deliberate behavior change with its own commit + snapshot update. +""" +import importlib + + +def _module(name: str): + for prefix in ("lnbits.extensions.libra", "libra"): + try: + return importlib.import_module(f"{prefix}.{name}") + except ModuleNotFoundError: + continue + raise ModuleNotFoundError(f"libra.{name}: tried both import paths") + + +views_api = _module("views_api") + +EXPECTED_ROUTES = [ + ("GET", "/api/v1/currencies", "api_get_currencies"), + ("GET", "/api/v1/accounts", "api_get_accounts"), + ("POST", "/api/v1/accounts", "api_create_account"), + ("GET", "/api/v1/accounts/{account_id}", "api_get_account"), + ("GET", "/api/v1/accounts/{account_id}/balance", "api_get_account_balance"), + ("GET", "/api/v1/accounts/{account_id}/transactions", "api_get_account_transactions"), + ("GET", "/api/v1/entries", "api_get_journal_entries"), + ("GET", "/api/v1/entries/user", "api_get_user_entries"), + ("GET", "/api/v1/entries/pending", "api_get_pending_entries"), + ("POST", "/api/v1/entries", "api_create_journal_entry"), + ("POST", "/api/v1/entries/expense", "api_create_expense_entry"), + ("POST", "/api/v1/entries/income", "api_create_income_entry"), + ("POST", "/api/v1/entries/receivable", "api_create_receivable_entry"), + ("POST", "/api/v1/entries/revenue", "api_create_revenue_entry"), + ("GET", "/api/v1/balance", "api_get_my_balance"), + ("GET", "/api/v1/balance/{user_id}", "api_get_user_balance"), + ("GET", "/api/v1/balances/all", "api_get_all_balances"), + ("POST", "/api/v1/generate-payment-invoice", "api_generate_payment_invoice"), + ("POST", "/api/v1/record-payment", "api_record_payment"), + ("POST", "/api/v1/receivables/settle", "api_settle_receivable"), + ("POST", "/api/v1/payables/pay", "api_pay_user"), + ("GET", "/api/v1/settings", "api_get_settings"), + ("PUT", "/api/v1/settings", "api_update_settings"), + ("GET", "/api/v1/user-wallet/{user_id}", "api_get_user_wallet"), + ("GET", "/api/v1/users", "api_get_all_users"), + ("GET", "/api/v1/admin/libra-users", "api_get_libra_users"), + ("GET", "/api/v1/reports/expenses", "api_expense_report"), + ("GET", "/api/v1/reports/contributions", "api_contributions_report"), + ("GET", "/api/v1/users/{user_id}/unsettled-entries", "api_get_unsettled_entries"), + ("GET", "/api/v1/user/wallet", "api_get_user_wallet"), + ("PUT", "/api/v1/user/wallet", "api_update_user_wallet"), + ("POST", "/api/v1/manual-payment-request", "api_create_manual_payment_request"), + ("GET", "/api/v1/manual-payment-requests", "api_get_manual_payment_requests"), + ("GET", "/api/v1/manual-payment-requests/all", "api_get_all_manual_payment_requests"), + ("POST", "/api/v1/manual-payment-requests/{request_id}/approve", "api_approve_manual_payment_request"), + ("POST", "/api/v1/manual-payment-requests/{request_id}/reject", "api_reject_manual_payment_request"), + ("POST", "/api/v1/entries/{entry_id}/approve", "api_approve_expense_entry"), + ("POST", "/api/v1/entries/{entry_id}/reject", "api_reject_expense_entry"), + ("POST", "/api/v1/assertions", "api_create_balance_assertion"), + ("GET", "/api/v1/assertions", "api_get_balance_assertions"), + ("GET", "/api/v1/assertions/{assertion_id}", "api_get_balance_assertion"), + ("POST", "/api/v1/assertions/{assertion_id}/check", "api_check_balance_assertion"), + ("DELETE", "/api/v1/assertions/{assertion_id}", "api_delete_balance_assertion"), + ("GET", "/api/v1/reconciliation/summary", "api_get_reconciliation_summary"), + ("POST", "/api/v1/reconciliation/check-all", "api_check_all_assertions"), + ("GET", "/api/v1/reconciliation/discrepancies", "api_get_discrepancies"), + ("POST", "/api/v1/tasks/daily-reconciliation", "api_run_daily_reconciliation"), + ("GET", "/api/v1/user/info", "api_get_user_info"), + ("POST", "/api/v1/admin/equity-eligibility", "api_grant_equity_eligibility"), + ("DELETE", "/api/v1/admin/equity-eligibility/{user_id}", "api_revoke_equity_eligibility"), + ("GET", "/api/v1/admin/equity-eligibility", "api_list_equity_eligible_users"), + ("POST", "/api/v1/admin/permissions", "api_grant_permission"), + ("GET", "/api/v1/admin/permissions", "api_list_permissions"), + ("DELETE", "/api/v1/admin/permissions/{permission_id}", "api_revoke_permission"), + ("POST", "/api/v1/admin/permissions/bulk", "api_bulk_grant_permissions"), + ("POST", "/api/v1/admin/permissions/bulk-grant", "api_bulk_grant_permission_to_users"), + ("GET", "/api/v1/users/me/permissions", "api_get_user_permissions"), + ("GET", "/api/v1/accounts/hierarchy", "api_get_account_hierarchy"), + ("POST", "/api/v1/admin/accounts", "api_admin_add_chart_account"), + ("POST", "/api/v1/admin/accounts/sync", "api_sync_all_accounts"), + ("POST", "/api/v1/admin/accounts/sync/{account_name:path}", "api_sync_single_account"), + ("GET", "/api/v1/admin/roles", "api_get_all_roles"), + ("POST", "/api/v1/admin/roles", "api_create_role"), + ("GET", "/api/v1/admin/roles/{role_id}", "api_get_role"), + ("PUT", "/api/v1/admin/roles/{role_id}", "api_update_role"), + ("DELETE", "/api/v1/admin/roles/{role_id}", "api_delete_role"), + ("POST", "/api/v1/admin/roles/{role_id}/permissions", "api_add_role_permission"), + ("DELETE", "/api/v1/admin/roles/{role_id}/permissions/{permission_id}", "api_delete_role_permission"), + ("POST", "/api/v1/admin/user-roles", "api_assign_user_role"), + ("GET", "/api/v1/admin/user-roles/{user_id}", "api_get_user_roles"), + ("DELETE", "/api/v1/admin/user-roles/{user_role_id}", "api_revoke_user_role"), + ("GET", "/api/v1/admin/users/roles", "api_get_all_user_roles"), + ("GET", "/api/v1/users/me/roles", "api_get_my_roles"), +] + + +def test_route_table_matches_snapshot(): + actual = [ + (",".join(sorted(r.methods)), r.path, r.endpoint.__name__) + for r in views_api.libra_api_router.routes + ] + assert actual == EXPECTED_ROUTES