From e5a474fb38b9d3318aefa1a68138467eccf7b762 Mon Sep 17 00:00:00 2001 From: ZProger Date: Wed, 9 Sep 2026 01:04:52 +0300 Subject: [PATCH] =?UTF-8?q?test:=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D1=8C=20risk=5Flevel=20=D0=B8=20network=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=BE-=D1=85=D0=BE=D0=BB=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Создание с дефолтным/кастомным risk_level, правка через PATCH /assets/{id}, создание/правка/сброс network через PATCH /crypto/holdings/{id}, partial-update и 404 на неизвестный id. --- backend/tests/test_crypto.py | 120 +++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 12 deletions(-) diff --git a/backend/tests/test_crypto.py b/backend/tests/test_crypto.py index c4d1ec6..b768aa2 100644 --- a/backend/tests/test_crypto.py +++ b/backend/tests/test_crypto.py @@ -28,18 +28,26 @@ def _fake_fetch(prices: dict[str, "crypto_service._MarketPoint"]): return fetch -async def _add_bitcoin(client: AsyncClient, quantity: str = "0.5", price_per_unit: str = "40000") -> dict: - resp = await client.post( - "/crypto/holdings", - json={ - "coingecko_id": "bitcoin", - "symbol": "btc", - "name": "Bitcoin", - "quantity": quantity, - "price_per_unit": price_per_unit, - "date": "2026-01-01", - }, - ) +async def _add_bitcoin( + client: AsyncClient, + quantity: str = "0.5", + price_per_unit: str = "40000", + risk_level: str | None = None, + network: str | None = None, +) -> dict: + payload = { + "coingecko_id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "quantity": quantity, + "price_per_unit": price_per_unit, + "date": "2026-01-01", + } + if risk_level is not None: + payload["risk_level"] = risk_level + if network is not None: + payload["network"] = network + resp = await client.post("/crypto/holdings", json=payload) assert resp.status_code == 201, resp.text return resp.json() @@ -678,3 +686,91 @@ async def test_empty_archived_portfolio_can_be_deleted(client: AsyncClient): assert resp.status_code == 204 assert (await client.get("/crypto/portfolios")).json() == [] + + +async def test_create_holding_defaults_risk_level_to_high(client: AsyncClient, monkeypatch): + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + + holding = await _add_bitcoin(client) + + assert holding["risk_level"] == "high" + + +async def test_create_holding_accepts_a_custom_risk_level(client: AsyncClient, monkeypatch): + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + + holding = await _add_bitcoin(client, risk_level="low") + + assert holding["risk_level"] == "low" + + +async def test_holding_risk_level_is_editable_via_the_underlying_asset(client: AsyncClient, monkeypatch): + """risk_level lives on the Asset, not CryptoHolding — see + services/crypto_service.py's _to_read — so it's edited through the + existing PATCH /assets/{asset_id}, same endpoint every other Asset + already uses, rather than a crypto-specific route.""" + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + holding = await _add_bitcoin(client, risk_level="high") + + resp = await client.patch(f"/assets/{holding['asset_id']}", json={"risk_level": "medium"}) + assert resp.status_code == 200, resp.text + + refreshed = (await client.get("/crypto/holdings")).json()["holdings"][0] + assert refreshed["risk_level"] == "medium" + + +async def test_create_holding_defaults_network_to_null(client: AsyncClient, monkeypatch): + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + + holding = await _add_bitcoin(client) + + assert holding["network"] is None + + +async def test_create_holding_accepts_a_network(client: AsyncClient, monkeypatch): + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + + holding = await _add_bitcoin(client, network="Ethereum") + + assert holding["network"] == "Ethereum" + + +async def test_update_holding_network(client: AsyncClient, monkeypatch): + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + holding = await _add_bitcoin(client) + + resp = await client.patch(f"/crypto/holdings/{holding['asset_id']}", json={"network": "Tron"}) + + assert resp.status_code == 200, resp.text + assert resp.json()["network"] == "Tron" + refreshed = (await client.get("/crypto/holdings")).json()["holdings"][0] + assert refreshed["network"] == "Tron" + + +async def test_update_holding_network_can_be_cleared_with_null(client: AsyncClient, monkeypatch): + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + holding = await _add_bitcoin(client, network="Ethereum") + + resp = await client.patch(f"/crypto/holdings/{holding['asset_id']}", json={"network": None}) + + assert resp.status_code == 200, resp.text + assert resp.json()["network"] is None + + +async def test_update_holding_omitting_network_leaves_it_untouched(client: AsyncClient, monkeypatch): + """Partial PATCH semantics, same as every other endpoint in this app — + an empty body must not silently null out a field the caller didn't + mention (see schemas/crypto.py's CryptoHoldingUpdate).""" + monkeypatch.setattr(crypto_service, "_fetch_market_data", _fake_fetch({"bitcoin": _point("50000")})) + holding = await _add_bitcoin(client, network="Ethereum") + + resp = await client.patch(f"/crypto/holdings/{holding['asset_id']}", json={}) + + assert resp.status_code == 200, resp.text + assert resp.json()["network"] == "Ethereum" + + +async def test_update_holding_404_for_unknown_id(client: AsyncClient): + resp = await client.patch("/crypto/holdings/999999", json={"network": "Ethereum"}) + + assert resp.status_code == 404