from __future__ import annotations from datetime import date from decimal import Decimal from pathlib import Path import httpx import pytest from broker_sync.fx import FxCache from broker_sync.fx_ecb import ( fetch_ecb_rates, fetch_ecb_rates_historical, populate_fx_cache, ) from broker_sync.models import FxRateSource _FIXTURE = Path(__file__).parent / "fixtures" / "ecb_2026-04-01.xml" def _transport_serving_fixture() -> httpx.MockTransport: xml = _FIXTURE.read_text() def handler(req: httpx.Request) -> httpx.Response: assert req.url.host == "www.ecb.europa.eu" assert req.url.path.endswith("/eurofxref-daily.xml") return httpx.Response(200, content=xml, headers={"content-type": "application/xml"}) return httpx.MockTransport(handler) async def test_fetch_returns_dict_of_decimals() -> None: rates = await fetch_ecb_rates(date(2026, 4, 1), transport=_transport_serving_fixture()) # GBP is rebased to 1.0 (everything else is rate_to_GBP). assert rates["GBP"] == Decimal("1") # USD: ECB says EUR→USD = 1.0823, EUR→GBP = 0.8564. # Therefore USD→GBP = 0.8564 / 1.0823 ≈ 0.7913... assert rates["USD"] == Decimal("0.8564") / Decimal("1.0823") # EUR→GBP = 0.8564 directly. assert rates["EUR"] == Decimal("0.8564") # JPY is a tiny number: 0.8564 / 163.45 ≈ 0.00524 assert rates["JPY"] == Decimal("0.8564") / Decimal("163.45") async def test_fetch_rejects_non_2xx() -> None: def handler(req: httpx.Request) -> httpx.Response: return httpx.Response(503) with pytest.raises(RuntimeError, match="ECB"): await fetch_ecb_rates(date(2026, 4, 1), transport=httpx.MockTransport(handler)) async def test_populate_fx_cache_writes_non_gbp(tmp_path: Path) -> None: cache = FxCache(tmp_path / "fx.db") rates = { "GBP": Decimal("1"), "USD": Decimal("0.79"), "EUR": Decimal("0.86"), } await populate_fx_cache(cache, rates, date(2026, 4, 1)) # GBP is not stored — convert_to_gbp handles it via the passthrough rule. assert cache.get("GBP", date(2026, 4, 1)) is None usd = cache.get("USD", date(2026, 4, 1)) assert usd is not None assert usd[0] == Decimal("0.79") assert usd[1] is FxRateSource.ECB_LIVE eur = cache.get("EUR", date(2026, 4, 1)) assert eur is not None assert eur[0] == Decimal("0.86") async def test_fetch_historical_raises_not_implemented() -> None: with pytest.raises(NotImplementedError): await fetch_ecb_rates_historical(date(2020, 1, 1), date(2026, 1, 1))