"""Tests for PortfolioClient.""" from __future__ import annotations from decimal import Decimal import pytest from pytest_httpx import HTTPXMock from cerbero_bite.clients._base import HttpToolClient from cerbero_bite.clients._exceptions import McpDataAnomalyError from cerbero_bite.clients.portfolio import PortfolioClient def _client() -> PortfolioClient: http = HttpToolClient( service="portfolio", base_url="http://mcp-portfolio:9018", token="t", retry_max=1, ) return PortfolioClient(http) @pytest.mark.asyncio async def test_total_equity_eur(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="http://mcp-portfolio:9018/tools/get_total_portfolio_value", json={"total_value_eur": 12345.67}, ) out = await _client().total_equity_eur() assert out == Decimal("12345.67") @pytest.mark.asyncio async def test_total_equity_anomaly_when_missing(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(json={}) with pytest.raises(McpDataAnomalyError, match="total_value_eur"): await _client().total_equity_eur() @pytest.mark.asyncio async def test_total_equity_anomaly_on_unexpected_shape(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(json=[1, 2, 3]) with pytest.raises(McpDataAnomalyError, match="unexpected shape"): await _client().total_equity_eur() @pytest.mark.asyncio async def test_asset_pct_aggregates_matching_tickers(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( url="http://mcp-portfolio:9018/tools/get_holdings", json=[ {"ticker": "ETH-USD", "current_value_eur": 3000.0}, {"ticker": "ETHE", "current_value_eur": 1000.0}, # ETH ticker variant {"ticker": "AAPL", "current_value_eur": 6000.0}, ], ) pct = await _client().asset_pct_of_portfolio("ETH") # 4000 / 10000 = 0.4 assert pct == Decimal("0.4") @pytest.mark.asyncio async def test_asset_pct_returns_zero_for_empty_portfolio( httpx_mock: HTTPXMock, ) -> None: httpx_mock.add_response(json=[]) assert await _client().asset_pct_of_portfolio("ETH") == Decimal("0") @pytest.mark.asyncio async def test_asset_pct_skips_entries_without_value(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response( json=[ {"ticker": "ETH", "current_value_eur": None}, {"ticker": "AAPL", "current_value_eur": 1000.0}, ] ) assert await _client().asset_pct_of_portfolio("ETH") == Decimal("0") @pytest.mark.asyncio async def test_asset_pct_anomaly_when_response_not_list(httpx_mock: HTTPXMock) -> None: httpx_mock.add_response(json={"holdings": []}) with pytest.raises(McpDataAnomalyError, match="unexpected shape"): await _client().asset_pct_of_portfolio("ETH") def test_portfolio_client_rejects_wrong_service() -> None: bad = HttpToolClient( service="macro", base_url="http://x:1", token="t", retry_max=1 ) with pytest.raises(ValueError, match="requires service 'portfolio'"): PortfolioClient(bad)