feat(api): портфели, ручные события, правка инструментов и обзор по скоупам

CRUD портфелей (/portfolios), ручные события (POST /events, DELETE только для manual), PATCH /instruments/{id}, GET /analytics/overview — карточка на каждый скоуп одним запросом. Холдинги отдают logo_url и logo_color. openapi.json обновлён.
This commit is contained in:
Dmitry
2026-09-19 22:13:55 +03:00
parent 75933993b2
commit f1f336f94f
15 changed files with 2129 additions and 21 deletions
+113
View File
@@ -0,0 +1,113 @@
"""Portfolio CRUD and the account set over HTTP."""
from factories import make_account
URL = "/api/v1/portfolios"
async def test_create_list_and_replace_accounts(client, auth_headers):
a = await make_account(name="Брокер 1")
b = await make_account(name="Брокер 2")
r = await client.post(
URL, json={"name": " Основной ", "account_ids": [b, a]}, headers=auth_headers
)
assert r.status_code == 201, r.text
created = r.json()
assert created["name"] == "Основной"
assert created["base_currency"] == "RUB"
assert created["account_ids"] == sorted([a, b])
r = await client.get(URL, headers=auth_headers)
assert [p["id"] for p in r.json()] == [created["id"]]
r = await client.put(
f"{URL}/{created['id']}/accounts", json={"account_ids": [b]}, headers=auth_headers
)
assert r.status_code == 200, r.text
assert r.json()["account_ids"] == [b]
r = await client.put(
f"{URL}/{created['id']}/accounts", json={"account_ids": []}, headers=auth_headers
)
assert r.json()["account_ids"] == []
async def test_create_without_accounts_is_allowed(client, auth_headers):
r = await client.post(URL, json={"name": "Пустой"}, headers=auth_headers)
assert r.status_code == 201
assert r.json()["account_ids"] == []
async def test_duplicate_name_is_a_conflict(client, auth_headers):
await client.post(URL, json={"name": "Основной"}, headers=auth_headers)
r = await client.post(URL, json={"name": "Основной"}, headers=auth_headers)
assert r.status_code == 409
assert r.headers["content-type"].startswith("application/problem+json")
async def test_rename_and_rename_conflict(client, auth_headers):
first = (await client.post(URL, json={"name": "Один"}, headers=auth_headers)).json()
await client.post(URL, json={"name": "Два"}, headers=auth_headers)
r = await client.patch(
f"{URL}/{first['id']}", json={"name": "Один и один"}, headers=auth_headers
)
assert r.status_code == 200
assert r.json()["name"] == "Один и один"
r = await client.patch(f"{URL}/{first['id']}", json={"name": "Два"}, headers=auth_headers)
assert r.status_code == 409
r = await client.patch(
f"{URL}/{first['id']}", json={"name": "Один и один"}, headers=auth_headers
)
assert r.status_code == 200 # keeping its own name is not a conflict
async def test_blank_name_is_rejected(client, auth_headers):
r = await client.post(URL, json={"name": " "}, headers=auth_headers)
assert r.status_code == 400
async def test_unknown_account_is_rejected_and_nothing_is_saved(client, auth_headers):
real = await make_account(name="Брокер")
r = await client.post(
URL, json={"name": "Х", "account_ids": [real, 99999]}, headers=auth_headers
)
assert r.status_code == 400
assert "99999" in r.json()["detail"]
assert (await client.get(URL, headers=auth_headers)).json() == []
created = (
await client.post(URL, json={"name": "Х", "account_ids": [real]}, headers=auth_headers)
).json()
r = await client.put(
f"{URL}/{created['id']}/accounts", json={"account_ids": [99999]}, headers=auth_headers
)
assert r.status_code == 400
listed = (await client.get(URL, headers=auth_headers)).json()
assert listed[0]["account_ids"] == [real]
async def test_delete_removes_the_portfolio_but_not_its_accounts(client, auth_headers):
a = await make_account(name="Брокер")
created = (
await client.post(URL, json={"name": "Х", "account_ids": [a]}, headers=auth_headers)
).json()
r = await client.delete(f"{URL}/{created['id']}", headers=auth_headers)
assert r.status_code == 204
assert (await client.get(URL, headers=auth_headers)).json() == []
accounts = (await client.get("/api/v1/accounts", headers=auth_headers)).json()
assert [x["id"] for x in accounts] == [a]
async def test_missing_portfolio_is_404(client, auth_headers):
assert (
await client.patch(f"{URL}/999", json={"name": "Х"}, headers=auth_headers)
).status_code == 404
assert (
await client.put(f"{URL}/999/accounts", json={"account_ids": []}, headers=auth_headers)
).status_code == 404
assert (await client.delete(f"{URL}/999", headers=auth_headers)).status_code == 404