Префикс /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), мигрирует его один раз на сессию и усекает таблицы после каждого теста.
82 lines
3.0 KiB
Python
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}
|