feat: add market data architecture and complete migration through build 039
This commit is contained in:
118
app/tests/unit/integrations/exchange/test_service_quote.py
Normal file
118
app/tests/unit/integrations/exchange/test_service_quote.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user