Files
Dmitry 3fc7a954b9 feat(api): FastAPI — auth, RFC 7807 и роуты фазы 1
Префикс /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),
мигрирует его один раз на сессию и усекает таблицы после каждого теста.
2026-09-18 13:43:49 +03:00

82 lines
3.0 KiB
Python

from datetime import timedelta
from factories import make_account, make_txn
from fintracker.analytics import today_local
async def test_rules_crud(client, auth_headers):
r = await client.post(
"/api/v1/rules",
headers=auth_headers,
json={"kind": "savings", "match_type": "payee", "pattern": "Копилка", "priority": 10},
)
assert r.status_code == 201
rule_id = r.json()["id"]
assert r.json()["enabled"] is True
assert r.json()["match_count"] == 0
r = await client.get("/api/v1/rules", headers=auth_headers)
assert [x["id"] for x in r.json()] == [rule_id]
r = await client.patch(
f"/api/v1/rules/{rule_id}", headers=auth_headers, json={"pattern": "Копилка%"}
)
assert r.status_code == 200 and r.json()["pattern"] == "Копилка%"
r = await client.patch(f"/api/v1/rules/{rule_id}", headers=auth_headers, json={"kind": None})
assert r.status_code == 400
r = await client.post(
"/api/v1/rules",
headers=auth_headers,
json={"kind": "nonsense", "match_type": "payee", "pattern": "x"},
)
assert r.status_code == 422
r = await client.delete(f"/api/v1/rules/{rule_id}", headers=auth_headers)
assert r.status_code == 204
r = await client.delete(f"/api/v1/rules/{rule_id}", headers=auth_headers)
assert r.status_code == 404
assert (await client.get("/api/v1/rules", headers=auth_headers)).json() == []
async def test_apply_runs_the_refresh_and_reports_stale_rules(client, auth_headers):
card = await make_account(name="Карта", balance="10000")
await make_txn(
today_local() - timedelta(days=2),
outcome="5000",
outcome_account_id=card,
payee="Копилка",
)
matching = (
await client.post(
"/api/v1/rules",
headers=auth_headers,
json={"kind": "savings", "match_type": "payee", "pattern": "копилка"},
)
).json()["id"]
rotten = (
await client.post(
"/api/v1/rules",
headers=auth_headers,
json={"kind": "one_off", "match_type": "payee", "pattern": "Ничего не совпадает"},
)
).json()["id"]
r = await client.post("/api/v1/rules/apply", headers=auth_headers)
assert r.status_code == 202
assert r.json()["error"] is None
assert r.json()["trigger"] == "rules"
by_id = {x["id"]: x for x in (await client.get("/api/v1/rules", headers=auth_headers)).json()}
assert by_id[matching]["match_count"] == 1
assert by_id[matching]["last_matched_at"] is not None
assert by_id[rotten]["match_count"] == 0
r = await client.get("/api/v1/rules/stale", headers=auth_headers)
assert [x["id"] for x in r.json()] == [rotten]
quality = (await client.get("/api/v1/data-quality", headers=auth_headers)).json()
stale = [row for row in quality if row["check_name"] == "stale_rule"]
assert len(stale) == 1 and stale[0]["ref"] == {"rule_id": rotten}