test: real backend pytest + E2E Playwright suites, both isolated from prod data

backend/tests/: 33 pytest tests against a real Postgres database
(aurum_test, created/migrated/dropped per run) exercised through the actual
HTTP API — not sqlite, not mocks, and the real `aurum` database is never
opened (app lifespan is skipped, get_session is overridden per test).
Covers transaction category-kind validation, year/month/date-range
filtering and pagination, dashboard summary arithmetic, budget status, cash
flow, and the /transactions/years picker.

e2e/: 6 Playwright specs driving a real browser against a fully separate
throwaway docker-compose stack (run.sh: separate project name, database,
port, and volume, torn down with `down -v` on exit) — regression coverage
for the Recent Transactions month bug, the Card grid-overflow bug, the
frozen-year bug, the Net Worth default range, and the Dashboard->
Transactions deep link, all fixed earlier this session.

Also synced the backend's APP_VERSION constant (stale at 0.15.2, embedded
in exported backups) with the frontend version.
This commit is contained in:
ZProger
2026-08-16 22:56:53 +03:00
parent 98c61d50d8
commit 8e46726b4d
24 changed files with 1104 additions and 2 deletions

View File

@@ -0,0 +1,82 @@
"""Budgets: category-kind restriction, one-budget-per-category, and the
spent/remaining/over-budget math behind the Budget page's progress bars.
"""
from decimal import Decimal
from httpx import AsyncClient
from tests.helpers import money, txn_payload
async def test_budget_rejects_income_category(client: AsyncClient, categories):
resp = await client.post(
"/budgets", json={"category_id": categories["Salary"]["id"], "monthly_limit": "500.00"}
)
assert resp.status_code == 400
async def test_budget_rejects_duplicate_category(client: AsyncClient, categories):
category_id = categories["Groceries"]["id"]
first = await client.post("/budgets", json={"category_id": category_id, "monthly_limit": "300.00"})
assert first.status_code == 201
second = await client.post("/budgets", json={"category_id": category_id, "monthly_limit": "400.00"})
assert second.status_code == 400
async def test_status_reports_spent_remaining_and_over_budget(client: AsyncClient, account_id, categories):
category_id = categories["Groceries"]["id"]
await client.post("/budgets", json={"category_id": category_id, "monthly_limit": "100.00"})
for amount in ["60.00", "60.00"]: # 120.00 total, over a 100.00 limit
await client.post(
"/transactions",
json=txn_payload(account_id, amount=amount, category_id=category_id, date="2026-08-10"),
)
resp = await client.get("/budgets/status", params={"year": 2026, "month": 8})
item = resp.json()["items"][0]
assert money(item["spent"]) == Decimal("120.00")
assert money(item["remaining"]) == Decimal("-20.00")
assert item["percent"] == 120.0
assert item["is_over_budget"] is True
async def test_status_only_counts_the_selected_month(client: AsyncClient, account_id, categories):
category_id = categories["Groceries"]["id"]
await client.post("/budgets", json={"category_id": category_id, "monthly_limit": "100.00"})
await client.post(
"/transactions", json=txn_payload(account_id, amount="90.00", category_id=category_id, date="2026-07-15")
)
resp = await client.get("/budgets/status", params={"year": 2026, "month": 8})
item = resp.json()["items"][0]
assert money(item["spent"]) == 0
assert item["is_over_budget"] is False
async def test_status_is_empty_list_when_no_budgets_exist(client: AsyncClient):
resp = await client.get("/budgets/status", params={"year": 2026, "month": 8})
assert resp.json()["items"] == []
async def test_update_budget_changes_the_limit(client: AsyncClient, categories):
category_id = categories["Groceries"]["id"]
created = await client.post("/budgets", json={"category_id": category_id, "monthly_limit": "100.00"})
budget_id = created.json()["id"]
resp = await client.patch(f"/budgets/{budget_id}", json={"monthly_limit": "250.00"})
assert resp.status_code == 200
assert money(resp.json()["monthly_limit"]) == Decimal("250.00")
async def test_delete_budget_removes_it_from_status(client: AsyncClient, account_id, categories):
category_id = categories["Groceries"]["id"]
created = await client.post("/budgets", json={"category_id": category_id, "monthly_limit": "100.00"})
budget_id = created.json()["id"]
delete_resp = await client.delete(f"/budgets/{budget_id}")
assert delete_resp.status_code == 204
status = await client.get("/budgets/status", params={"year": 2026, "month": 8})
assert status.json()["items"] == []