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,194 @@
|
||||
"""Events entered by hand: derived signs, validation, and deletion of only the manual ones."""
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from factories import make_account, make_event, make_instrument, refresh
|
||||
from fintracker.models import AccountKind, AccountRole, EventKind
|
||||
|
||||
URL = "/api/v1/events"
|
||||
D = Decimal
|
||||
DAY = "2026-03-10"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def broker(app) -> int:
|
||||
return await make_account(
|
||||
name="Брокер", kind=AccountKind.broker, role=AccountRole.investment, balance=None
|
||||
)
|
||||
|
||||
|
||||
async def post(client, headers, **body):
|
||||
return await client.post(URL, json=body, headers=headers)
|
||||
|
||||
|
||||
async def test_buy_adds_the_position_and_takes_cash_including_the_fee(client, auth_headers, broker):
|
||||
sber = await make_instrument(ticker="SBER")
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind="buy", trade_date=DAY,
|
||||
instrument_id=sber, quantity="10", price="100", fee="5",
|
||||
) # fmt: skip
|
||||
assert r.status_code == 201, r.text
|
||||
e = r.json()
|
||||
assert D(e["quantity"]) == 10
|
||||
assert D(e["amount"]) == -1005
|
||||
assert (e["status"], e["ticker"], e["currency"]) == ("confirmed", "SBER", "RUB")
|
||||
assert e["source"] == "manual"
|
||||
assert D(e["fee"]) == 5
|
||||
|
||||
|
||||
async def test_sell_removes_from_the_position_and_brings_cash_less_the_fee(
|
||||
client, auth_headers, broker
|
||||
):
|
||||
sber = await make_instrument(ticker="SBER")
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind="sell", trade_date=DAY,
|
||||
instrument_id=sber, quantity="4", price="120", fee="2",
|
||||
) # fmt: skip
|
||||
e = r.json()
|
||||
assert D(e["quantity"]) == -4
|
||||
assert D(e["amount"]) == 478 # 4 × 120 − 2
|
||||
|
||||
|
||||
async def test_a_bond_purchase_adds_accrued_interest(client, auth_headers, broker):
|
||||
bond = await make_instrument(ticker="SU26207", name="ОФЗ")
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind="buy", trade_date=DAY,
|
||||
instrument_id=bond, quantity="50", price="967.94", accrued_interest="804",
|
||||
) # fmt: skip
|
||||
assert D(r.json()["amount"]) == D("-49201.00") # 50 × 967.94 + 804
|
||||
|
||||
|
||||
async def test_an_explicit_total_wins_over_the_computed_one(client, auth_headers, broker):
|
||||
sber = await make_instrument(ticker="SBER")
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind="buy", trade_date=DAY,
|
||||
instrument_id=sber, quantity="10", amount="1007.31",
|
||||
) # fmt: skip
|
||||
assert r.status_code == 201
|
||||
assert D(r.json()["amount"]) == D("-1007.31")
|
||||
|
||||
|
||||
async def test_cash_events_are_signed_by_their_kind(client, auth_headers, broker):
|
||||
sber = await make_instrument(ticker="SBER")
|
||||
expected = {
|
||||
"deposit": 500,
|
||||
"withdrawal": -500,
|
||||
"commission": -500,
|
||||
"tax": -500,
|
||||
"tax_refund": 500,
|
||||
}
|
||||
for kind, amount in expected.items():
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind=kind, trade_date=DAY, amount="500"
|
||||
)
|
||||
assert r.status_code == 201, (kind, r.text)
|
||||
assert D(r.json()["amount"]) == amount, kind
|
||||
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind="dividend", trade_date=DAY,
|
||||
instrument_id=sber, amount="87.5",
|
||||
) # fmt: skip
|
||||
assert D(r.json()["amount"]) == D("87.5")
|
||||
assert r.json()["quantity"] is None
|
||||
|
||||
|
||||
async def test_a_securities_transfer_moves_the_position_without_cash(client, auth_headers, broker):
|
||||
five = await make_instrument(ticker="FIVE")
|
||||
x5 = await make_instrument(ticker="X5")
|
||||
out = await post(
|
||||
client, auth_headers, account_id=broker, kind="transfer_out", trade_date=DAY,
|
||||
instrument_id=five, quantity="3",
|
||||
) # fmt: skip
|
||||
into = await post(
|
||||
client, auth_headers, account_id=broker, kind="transfer_in", trade_date=DAY,
|
||||
instrument_id=x5, quantity="3",
|
||||
) # fmt: skip
|
||||
assert (D(out.json()["quantity"]), D(out.json()["amount"])) == (-3, 0)
|
||||
assert (D(into.json()["quantity"]), D(into.json()["amount"])) == (3, 0)
|
||||
|
||||
|
||||
async def test_a_manual_purchase_opens_a_lot_and_a_deposit_is_a_flow(client, auth_headers, broker):
|
||||
sber = await make_instrument(ticker="SBER")
|
||||
await post(
|
||||
client, auth_headers, account_id=broker, kind="deposit", trade_date=DAY, amount="2000"
|
||||
)
|
||||
await post(
|
||||
client, auth_headers, account_id=broker, kind="buy", trade_date=DAY,
|
||||
instrument_id=sber, quantity="10", price="100",
|
||||
) # fmt: skip
|
||||
await refresh()
|
||||
|
||||
detail = (await client.get(f"/api/v1/instruments/{sber}", headers=auth_headers)).json()
|
||||
assert [D(lot["qty_remaining"]) for lot in detail["lots"]] == [10]
|
||||
listed = (await client.get(URL, params={"account_id": broker}, headers=auth_headers)).json()
|
||||
flows = [e for e in listed["items"] if e["external_flow"]]
|
||||
assert [e["kind"] for e in flows] == ["deposit"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("body", "status"),
|
||||
[
|
||||
({"kind": "buy", "quantity": "-1", "price": "1"}, 422), # sign comes from the kind
|
||||
({"kind": "buy", "price": "1"}, 422), # no quantity
|
||||
({"kind": "buy", "quantity": "1"}, 422), # neither price nor total
|
||||
({"kind": "deposit"}, 422), # no amount
|
||||
({"kind": "deposit", "amount": "0"}, 422),
|
||||
({"kind": "deposit", "amount": "-5"}, 422),
|
||||
({"kind": "split", "quantity": "2"}, 422), # not typed by hand
|
||||
({"kind": "fx_exchange", "amount": "5"}, 422),
|
||||
({"kind": "transfer_in", "quantity": "1"}, 422), # a transfer needs an instrument
|
||||
({"kind": "dividend", "amount": "5"}, 422), # so does a dividend
|
||||
],
|
||||
)
|
||||
async def test_invalid_events_are_refused(client, auth_headers, broker, body, status):
|
||||
r = await post(client, auth_headers, account_id=broker, trade_date=DAY, **body)
|
||||
assert r.status_code == status, r.text
|
||||
assert r.headers["content-type"].startswith("application/problem+json")
|
||||
assert (await client.get(URL, headers=auth_headers)).json()["total"] == 0
|
||||
|
||||
|
||||
async def test_account_and_instrument_must_exist_and_fit(client, auth_headers, broker):
|
||||
sber = await make_instrument(ticker="SBER")
|
||||
card = await make_account(name="Карта") # a ZenMoney account has no ledger
|
||||
|
||||
r = await post(
|
||||
client, auth_headers, account_id=card, kind="deposit", trade_date=DAY, amount="5"
|
||||
)
|
||||
assert r.status_code == 400
|
||||
r = await post(
|
||||
client, auth_headers, account_id=99999, kind="deposit", trade_date=DAY, amount="5"
|
||||
)
|
||||
assert r.status_code == 400
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind="buy", trade_date=DAY,
|
||||
instrument_id=99999, quantity="1", price="1",
|
||||
) # fmt: skip
|
||||
assert r.status_code == 400
|
||||
r = await post(
|
||||
client, auth_headers, account_id=broker, kind="deposit", trade_date=DAY,
|
||||
instrument_id=sber, amount="5",
|
||||
) # fmt: skip
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
async def test_only_manual_events_can_be_deleted(client, auth_headers, broker):
|
||||
broker_event = await make_event(
|
||||
date(2026, 3, 1), account_id=broker, kind=EventKind.deposit, amount=100
|
||||
)
|
||||
mine = (
|
||||
await post(
|
||||
client, auth_headers, account_id=broker, kind="deposit", trade_date=DAY, amount="5"
|
||||
)
|
||||
).json()["id"]
|
||||
|
||||
assert (await client.delete(f"{URL}/{mine}", headers=auth_headers)).status_code == 204
|
||||
r = await client.delete(f"{URL}/{broker_event}", headers=auth_headers)
|
||||
assert r.status_code == 409
|
||||
assert r.headers["content-type"].startswith("application/problem+json")
|
||||
assert (await client.delete(f"{URL}/{mine}", headers=auth_headers)).status_code == 404
|
||||
|
||||
remaining = (await client.get(URL, headers=auth_headers)).json()["items"]
|
||||
assert [e["id"] for e in remaining] == [broker_event]
|
||||
@@ -0,0 +1,109 @@
|
||||
"""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
|
||||
@@ -0,0 +1,87 @@
|
||||
"""One card per scope for the home screen."""
|
||||
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from factories import make_account, make_event, make_instrument, make_price, refresh
|
||||
from fintracker.analytics import today_local
|
||||
from fintracker.models import AccountKind, AccountRole, EventKind
|
||||
|
||||
URL = "/api/v1/analytics/overview"
|
||||
|
||||
|
||||
async def _broker(name: str) -> int:
|
||||
return await make_account(
|
||||
name=name, kind=AccountKind.broker, role=AccountRole.investment, balance=None
|
||||
)
|
||||
|
||||
|
||||
async def _invest(account: int, *, today_price: str) -> None:
|
||||
"""10 000 ₽ in, 100 shares at 100; the price is flat until today, when it is `today_price`."""
|
||||
t = today_local()
|
||||
bought = t - timedelta(days=5)
|
||||
sber = await make_instrument(ticker=f"T{account}")
|
||||
await make_event(bought, account_id=account, kind=EventKind.deposit, amount=10000)
|
||||
await make_event(
|
||||
bought, account_id=account, kind=EventKind.buy, instrument_id=sber,
|
||||
quantity=100, price=100, amount=-10000,
|
||||
) # fmt: skip
|
||||
d = bought
|
||||
while d < t:
|
||||
await make_price(d, instrument_id=sber, close="100")
|
||||
d += timedelta(days=1)
|
||||
await make_price(t, instrument_id=sber, close=today_price)
|
||||
|
||||
|
||||
async def test_cards_carry_value_result_and_the_change_over_the_last_day(client, auth_headers):
|
||||
account = await _broker("Брокер")
|
||||
await _invest(account, today_price="105")
|
||||
await refresh()
|
||||
|
||||
r = await client.get(URL, headers=auth_headers)
|
||||
assert r.status_code == 200, r.text
|
||||
cards = {c["scope"]: c for c in r.json()}
|
||||
card = cards[f"account:{account}"]
|
||||
assert card["name"] == "Брокер"
|
||||
assert card["kind"] == "account"
|
||||
assert Decimal(card["total_rub"]) == 10500
|
||||
assert Decimal(card["invested_rub"]) == 10000
|
||||
assert Decimal(card["pnl_rub"]) == 500
|
||||
assert Decimal(card["pnl_pct"]) == Decimal("0.05")
|
||||
assert Decimal(card["day_change_rub"]) == 500
|
||||
assert Decimal(card["day_change_pct"]) == Decimal("500") / Decimal("10000")
|
||||
assert Decimal(card["income_year_rub"]) == 0
|
||||
|
||||
|
||||
async def test_all_comes_first_then_portfolios_then_accounts_by_value(client, auth_headers):
|
||||
small, big = await _broker("Малый"), await _broker("Большой")
|
||||
await _invest(small, today_price="100")
|
||||
await _invest(big, today_price="130")
|
||||
created = await client.post(
|
||||
"/api/v1/portfolios", json={"name": "Мой", "account_ids": [small]}, headers=auth_headers
|
||||
)
|
||||
assert created.status_code == 201
|
||||
await refresh()
|
||||
|
||||
cards = (await client.get(URL, headers=auth_headers)).json()
|
||||
assert [c["kind"] for c in cards] == ["all", "portfolio", "account", "account"]
|
||||
assert [c["name"] for c in cards[2:]] == ["Большой", "Малый"]
|
||||
assert Decimal(cards[0]["total_rub"]) == Decimal(cards[2]["total_rub"]) + Decimal(
|
||||
cards[3]["total_rub"]
|
||||
)
|
||||
|
||||
|
||||
async def test_an_account_switched_off_has_no_card(client, auth_headers):
|
||||
kept, off = await _broker("Оставить"), await _broker("Отключить")
|
||||
await _invest(kept, today_price="100")
|
||||
await _invest(off, today_price="100")
|
||||
await client.patch(f"/api/v1/accounts/{off}", json={"disabled": True}, headers=auth_headers)
|
||||
await refresh()
|
||||
|
||||
names = [c["name"] for c in (await client.get(URL, headers=auth_headers)).json()]
|
||||
assert "Оставить" in names
|
||||
assert "Отключить" not in names
|
||||
|
||||
|
||||
async def test_nothing_built_yet_is_an_empty_list(client, auth_headers):
|
||||
assert (await client.get(URL, headers=auth_headers)).json() == []
|
||||
@@ -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