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:
@@ -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
|
||||
Reference in New Issue
Block a user