from __future__ import annotations from datetime import datetime, timezone import pytest from app.services.price_feed import PriceData, PriceFeed class _CacheStub: def __init__(self, payloads: dict[str, object] | None = None, enabled: bool = True) -> None: self.payloads = payloads or {} self.enabled = enabled self.writes: list[tuple[str, object, int | None]] = [] async def get_json(self, key: str): return self.payloads.get(key) async def set_json(self, key: str, value, ttl: int | None = None): self.payloads[key] = value self.writes.append((key, value, ttl)) @pytest.mark.asyncio async def test_price_feed_uses_normalized_cached_payload() -> None: feed = PriceFeed() feed._cache = _CacheStub( { "price:GLD": { "symbol": "GLD", "price": 200.0, "currency": "usd", "timestamp": "2026-03-26T12:00:00+00:00", "source": "cache", } } ) data = await feed.get_price("gld") assert data == PriceData( symbol="GLD", price=200.0, currency="USD", timestamp=datetime.fromisoformat("2026-03-26T12:00:00+00:00"), source="cache", ) @pytest.mark.asyncio async def test_price_feed_discards_malformed_cached_payload_and_refetches(monkeypatch: pytest.MonkeyPatch) -> None: feed = PriceFeed() feed._cache = _CacheStub({"price:GLD": {"symbol": "TLT", "price": 200.0, "timestamp": "2026-03-26T12:00:00+00:00"}}) async def fake_fetch(symbol: str): return { "symbol": symbol, "price": 201.5, "currency": "USD", "timestamp": datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc), "source": "yfinance", } monkeypatch.setattr(feed, "_fetch_yfinance", fake_fetch) data = await feed.get_price("GLD") assert data == PriceData( symbol="GLD", price=201.5, currency="USD", timestamp=datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc), source="yfinance", ) assert feed._cache.writes[0][0] == "price:GLD" @pytest.mark.asyncio async def test_price_feed_discards_cached_payload_missing_required_price_and_refetches( monkeypatch: pytest.MonkeyPatch, ) -> None: feed = PriceFeed() feed._cache = _CacheStub({"price:GLD": {"symbol": "GLD", "timestamp": "2026-03-26T12:00:00+00:00"}}) async def fake_fetch(symbol: str): return { "symbol": symbol, "price": 205.0, "currency": "USD", "timestamp": datetime(2026, 3, 26, 12, 3, tzinfo=timezone.utc), "source": "yfinance", } monkeypatch.setattr(feed, "_fetch_yfinance", fake_fetch) data = await feed.get_price("GLD") assert data == PriceData( symbol="GLD", price=205.0, currency="USD", timestamp=datetime(2026, 3, 26, 12, 3, tzinfo=timezone.utc), source="yfinance", ) @pytest.mark.asyncio async def test_price_feed_rejects_invalid_provider_payload() -> None: feed = PriceFeed() feed._cache = _CacheStub(enabled=False) async def fake_fetch(symbol: str): return { "symbol": symbol, "price": 0.0, "currency": "USD", "timestamp": datetime(2026, 3, 26, 12, 1, tzinfo=timezone.utc), "source": "yfinance", } feed._fetch_yfinance = fake_fetch # type: ignore[method-assign] data = await feed.get_price("GLD") assert data is None @pytest.mark.asyncio async def test_price_feed_caches_normalized_provider_payload(monkeypatch: pytest.MonkeyPatch) -> None: feed = PriceFeed() feed._cache = _CacheStub() async def fake_fetch(symbol: str): return { "symbol": symbol, "price": 202.25, "currency": "usd", "timestamp": datetime(2026, 3, 26, 12, 2, tzinfo=timezone.utc), "source": "yfinance", } monkeypatch.setattr(feed, "_fetch_yfinance", fake_fetch) data = await feed.get_price("GLD") assert data is not None assert data.currency == "USD" cache_key, cache_payload, ttl = feed._cache.writes[0] assert cache_key == "price:GLD" assert ttl == PriceFeed.CACHE_TTL_SECONDS assert cache_payload == { "symbol": "GLD", "price": 202.25, "currency": "USD", "timestamp": "2026-03-26T12:02:00+00:00", "source": "yfinance", }