# app/tests/unit/integrations/exchange/test_service_quote.py from __future__ import annotations from datetime import datetime, timedelta, timezone from decimal import Decimal from types import SimpleNamespace from typing import cast import pytest from src.core.config import Settings from src.integrations.exchange.exceptions import ExchangeError from src.integrations.exchange.market_cache import MarketPriceCache from src.integrations.exchange.service import ExchangeService from src.market_data.acquisition.models.quote import Quote from src.storage.quote_store import InMemoryQuoteStore def _service(*, exchange_enabled: bool = True) -> ExchangeService: service = object.__new__(ExchangeService) service.settings = cast( Settings, SimpleNamespace( exchange_enabled=exchange_enabled, default_symbol="BTC/USD_LEVERAGE", ), ) return service def _quote(*, age_seconds: float = 0.0) -> Quote: return Quote( symbol="BTC/USD_LEVERAGE", last_price=Decimal("100"), bid_price=Decimal("99"), ask_price=Decimal("101"), exchange_timestamp=None, received_at=( datetime.now(timezone.utc) - timedelta(seconds=age_seconds) ), source="test", ) @pytest.fixture(autouse=True) def reset_store(): original = MarketPriceCache._store MarketPriceCache._store = InMemoryQuoteStore() yield MarketPriceCache._store = original def test_get_quote_returns_fresh_cached_identity( monkeypatch: pytest.MonkeyPatch, ) -> None: service = _service() quote = _quote() MarketPriceCache.set_quote(quote, runtime_key="auto") monkeypatch.setattr( service, "validate_symbol", lambda _: SimpleNamespace( is_valid=True, normalized_symbol=quote.symbol, message="", ), ) monkeypatch.setattr( service, "_get_fresh_quote", lambda _: (_ for _ in ()).throw(AssertionError("REST not expected")), ) assert service.get_quote(quote.symbol, runtime_key="auto") is quote def test_get_quote_refreshes_stale_quote( monkeypatch: pytest.MonkeyPatch, ) -> None: service = _service() stale = _quote(age_seconds=10) fresh = _quote() MarketPriceCache.set_quote(stale, runtime_key="auto") monkeypatch.setattr( service, "validate_symbol", lambda _: SimpleNamespace( is_valid=True, normalized_symbol=fresh.symbol, message="", ), ) monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh) result = service.get_quote(fresh.symbol, runtime_key="auto") assert result is fresh assert MarketPriceCache.get_quote(fresh.symbol, runtime_key="auto") is fresh def test_get_quote_wraps_acquisition_error( monkeypatch: pytest.MonkeyPatch, ) -> None: service = _service() original = RuntimeError("unavailable") monkeypatch.setattr( service, "_load_quote_via_acquisition", lambda _: (_ for _ in ()).throw(original), ) monkeypatch.setattr(service, "_log_exchange_error", lambda **_: None) with pytest.raises(ExchangeError) as exc_info: service._get_fresh_quote("BTC/USD_LEVERAGE") assert exc_info.value.__cause__ is original