build 039: complete Quotes Feed migration foundation

This commit is contained in:
2026-07-14 09:58:16 +03:00
parent 26deb861bc
commit 7b62873832
443 changed files with 80452 additions and 1335 deletions

View 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