Префикс /api/v1, operationId = "<tag>_<name>", чтобы Dart-клиент получил методы вроде accountsList, а не list_accounts_api_v1_accounts_get. Ошибки — application/problem+json. Auth: access-JWT на час + refresh на 30 дней, который хранится хэшем и ротируется, логин ограничен по частоте в памяти. Деньги в JSON — всегда позиционные строки (api/schemas/common.py): float в проводе потерял бы копейки, которые NUMERIC(24,10) бережёт. Тестовый harness поднимает свой Postgres через pg_ctl (pytest-postgresql), мигрирует его один раз на сессию и усекает таблицы после каждого теста.
111 lines
4.0 KiB
Python
111 lines
4.0 KiB
Python
from datetime import timedelta
|
|
|
|
from factories import make_account, make_category, make_cbr_rate, make_txn, refresh
|
|
from fintracker.analytics import today_local
|
|
|
|
|
|
async def test_pagination_filters_and_string_money(client, auth_headers):
|
|
card = await make_account(name="Карта", balance="0")
|
|
other = await make_account(name="Вклад", balance="0")
|
|
food = await make_category("Еда")
|
|
t = today_local()
|
|
await make_cbr_rate(t - timedelta(days=10), "USD", "90")
|
|
|
|
for i in range(1, 6):
|
|
await make_txn(
|
|
t - timedelta(days=i),
|
|
outcome=f"{i}00.55",
|
|
outcome_account_id=card,
|
|
payee=f"Магазин {i}",
|
|
primary_category_id=food if i == 1 else None,
|
|
)
|
|
await make_txn(
|
|
t - timedelta(days=6),
|
|
income="1000",
|
|
income_account_id=other,
|
|
payee="Зарплата",
|
|
comment="аванс",
|
|
)
|
|
await make_txn(
|
|
t - timedelta(days=7), outcome="10", outcome_currency="USD", outcome_account_id=card
|
|
)
|
|
await make_txn(t - timedelta(days=8), outcome="1", outcome_account_id=card, deleted=True)
|
|
await refresh()
|
|
|
|
r = await client.get(
|
|
"/api/v1/transactions", headers=auth_headers, params={"page_size": 3, "page": 1}
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["total"] == 7 # the deleted one is excluded
|
|
assert body["page"] == 1 and body["page_size"] == 3
|
|
assert len(body["items"]) == 3
|
|
dates = [i["date"] for i in body["items"]]
|
|
assert dates == sorted(dates, reverse=True)
|
|
|
|
first = body["items"][0]
|
|
assert isinstance(first["outcome"], str)
|
|
assert first["outcome"].startswith("100.55")
|
|
assert first["outcome_rub"].startswith("100.55")
|
|
assert first["flow_type"] == "expense"
|
|
|
|
page2 = await client.get(
|
|
"/api/v1/transactions", headers=auth_headers, params={"page_size": 3, "page": 2}
|
|
)
|
|
assert len(page2.json()["items"]) == 3
|
|
assert {i["id"] for i in page2.json()["items"]} & {i["id"] for i in body["items"]} == set()
|
|
|
|
# filters
|
|
r = await client.get("/api/v1/transactions", headers=auth_headers, params={"account_id": other})
|
|
assert [i["payee"] for i in r.json()["items"]] == ["Зарплата"]
|
|
|
|
r = await client.get("/api/v1/transactions", headers=auth_headers, params={"q": "аванс"})
|
|
assert r.json()["total"] == 1
|
|
|
|
r = await client.get("/api/v1/transactions", headers=auth_headers, params={"q": "магазин"})
|
|
assert r.json()["total"] == 5 # ILIKE, case-insensitive
|
|
|
|
r = await client.get(
|
|
"/api/v1/transactions", headers=auth_headers, params={"flow_type": "income"}
|
|
)
|
|
assert r.json()["total"] == 1
|
|
|
|
r = await client.get("/api/v1/transactions", headers=auth_headers, params={"category_id": food})
|
|
assert r.json()["total"] == 1
|
|
assert r.json()["items"][0]["tags"] == [food]
|
|
|
|
r = await client.get(
|
|
"/api/v1/transactions",
|
|
headers=auth_headers,
|
|
params={"from": str(t - timedelta(days=2)), "to": str(t)},
|
|
)
|
|
assert r.json()["total"] == 2
|
|
|
|
r = await client.get(
|
|
"/api/v1/transactions", headers=auth_headers, params={"include_deleted": True}
|
|
)
|
|
assert r.json()["total"] == 8
|
|
|
|
# the USD purchase converts at its own date's rate, and never silently at another
|
|
r = await client.get(
|
|
"/api/v1/transactions", headers=auth_headers, params={"q": "", "page_size": 100}
|
|
)
|
|
usd = next(i for i in r.json()["items"] if i["outcome_currency"] == "USD")
|
|
assert usd["outcome_rub"].startswith("900")
|
|
|
|
|
|
async def test_unquoted_currency_gives_null_rub(client, auth_headers):
|
|
card = await make_account(balance="0")
|
|
await make_txn(
|
|
today_local() - timedelta(days=1),
|
|
outcome="2",
|
|
outcome_currency="XBT",
|
|
outcome_account_id=card,
|
|
)
|
|
await refresh()
|
|
|
|
r = await client.get("/api/v1/transactions", headers=auth_headers)
|
|
(item,) = r.json()["items"]
|
|
assert item["outcome"] == "2.0000000000"
|
|
assert item["outcome_rub"] is None
|