CRUD портфелей (/portfolios), ручные события (POST /events, DELETE только для manual), PATCH /instruments/{id}, GET /analytics/overview — карточка на каждый скоуп одним запросом. Холдинги отдают logo_url и logo_color. openapi.json обновлён.
110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""Hand corrections to an instrument over HTTP."""
|
||
|
||
from factories import make_instrument
|
||
|
||
|
||
def url(instrument_id: int) -> str:
|
||
return f"/api/v1/instruments/{instrument_id}"
|
||
|
||
|
||
async def test_patch_lot_board_name_class_and_sector(client, auth_headers):
|
||
iid = await make_instrument(ticker="MSNG", board="TQBR")
|
||
|
||
r = await client.patch(
|
||
url(iid),
|
||
json={
|
||
"lot": 1000,
|
||
"name": " Мосэнерго ",
|
||
"asset_class": "bond",
|
||
"sector": "Энергетика",
|
||
"board": "TQTF",
|
||
},
|
||
headers=auth_headers,
|
||
)
|
||
assert r.status_code == 200, r.text
|
||
row = r.json()
|
||
assert (row["lot"], row["name"], row["asset_class"]) == (1000, "Мосэнерго", "bond")
|
||
assert (row["sector"], row["board"]) == ("Энергетика", "TQTF")
|
||
|
||
detail = (await client.get(url(iid), headers=auth_headers)).json()["instrument"]
|
||
assert detail["lot"] == 1000
|
||
|
||
|
||
async def test_unset_fields_are_left_alone_and_null_clears_board(client, auth_headers):
|
||
iid = await make_instrument(ticker="AFLT", board="TQBR", name="Аэрофлот")
|
||
|
||
r = await client.patch(url(iid), json={"lot": 10}, headers=auth_headers)
|
||
assert (r.json()["name"], r.json()["board"], r.json()["lot"]) == ("Аэрофлот", "TQBR", 10)
|
||
|
||
r = await client.patch(url(iid), json={"board": None}, headers=auth_headers)
|
||
assert r.status_code == 200
|
||
assert r.json()["board"] is None
|
||
assert r.json()["lot"] == 10
|
||
|
||
|
||
async def test_invalid_values_are_refused(client, auth_headers):
|
||
iid = await make_instrument(ticker="PHOR")
|
||
|
||
for body in ({"lot": 0}, {"lot": -5}, {"name": ""}, {"unknown_field": 1}):
|
||
r = await client.patch(url(iid), json=body, headers=auth_headers)
|
||
assert r.status_code == 422, body
|
||
|
||
for body in ({"lot": None}, {"name": None}, {"asset_class": None}, {"name": " "}):
|
||
r = await client.patch(url(iid), json=body, headers=auth_headers)
|
||
assert r.status_code == 400, body
|
||
|
||
r = await client.patch(url(iid), json={"asset_class": "stock"}, headers=auth_headers)
|
||
assert r.status_code == 422
|
||
assert "share" in r.json()["detail"]
|
||
|
||
|
||
async def test_moving_to_a_taken_ticker_and_board_is_a_conflict(client, auth_headers):
|
||
await make_instrument(ticker="SBER", board="TQBR")
|
||
other = await make_instrument(ticker="SBER", board="SMAL")
|
||
|
||
r = await client.patch(url(other), json={"board": "TQBR"}, headers=auth_headers)
|
||
assert r.status_code == 409
|
||
assert r.headers["content-type"].startswith("application/problem+json")
|
||
|
||
r = await client.patch(url(other), json={"board": "TQBR", "lot": 10}, headers=auth_headers)
|
||
assert r.status_code == 409 # nothing was half-applied
|
||
assert (await client.get(url(other), headers=auth_headers)).json()["instrument"]["lot"] == 1
|
||
|
||
|
||
async def test_missing_instrument_is_404(client, auth_headers):
|
||
r = await client.patch(url(999), json={"lot": 2}, headers=auth_headers)
|
||
assert r.status_code == 404
|
||
|
||
|
||
async def test_instrument_and_holding_expose_a_public_logo_url(client, auth_headers):
|
||
from datetime import date
|
||
|
||
from factories import make_account, make_event, refresh
|
||
from fintracker.db import get_sessionmaker
|
||
from fintracker.models import AccountKind, AccountRole, EventKind, Instrument
|
||
|
||
iid = await make_instrument(ticker="SBER")
|
||
async with get_sessionmaker()() as session:
|
||
row = await session.get(Instrument, iid)
|
||
assert row is not None
|
||
row.logo_name, row.logo_color = "sber.png", "#21A038"
|
||
await session.commit()
|
||
account = await make_account(
|
||
name="Т", kind=AccountKind.broker, role=AccountRole.investment, balance=None
|
||
)
|
||
await make_event(
|
||
date(2026, 3, 1), account_id=account, kind=EventKind.buy, instrument_id=iid,
|
||
quantity=1, price=100, amount=-100,
|
||
) # fmt: skip
|
||
await refresh()
|
||
|
||
card = (await client.get(url(iid), headers=auth_headers)).json()
|
||
assert card["instrument"]["logo_url"] == "https://invest-brands.cdn-tinkoff.ru/sberx160.png"
|
||
assert card["instrument"]["logo_color"] == "#21A038"
|
||
assert card["holding"]["logo_url"] == card["instrument"]["logo_url"]
|
||
|
||
plain = await make_instrument(ticker="NOLOGO")
|
||
assert (await client.get(url(plain), headers=auth_headers)).json()["instrument"][
|
||
"logo_url"
|
||
] is None
|