129 lines
3.6 KiB
Python
129 lines
3.6 KiB
Python
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_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",
|
|
}
|