"""Tests for DATA-004: Underlying Instrument Selector routing in DataService.""" from __future__ import annotations import pytest from app.services.cache import CacheService from app.services.data_service import DataService class _CacheStub(CacheService): """In-memory cache stub for unit testing.""" def __init__(self, initial: dict[str, object] | None = None) -> None: self._store = dict(initial or {}) self.write_count = 0 async def get_json(self, key: str): # type: ignore[override] return self._store.get(key) async def set_json(self, key: str, value): # type: ignore[override] self._store[key] = value self.write_count += 1 return True class TestUnderlyingRouting: """Test underlying instrument routing in DataService.""" @pytest.mark.asyncio async def test_data_service_default_underlying_is_gld(self) -> None: """DataService defaults to GLD underlying.""" cache = _CacheStub() service = DataService(cache) assert service.default_underlying == "GLD" @pytest.mark.asyncio async def test_data_service_default_symbol_alias(self) -> None: """default_symbol is an alias for default_underlying (backward compatibility).""" cache = _CacheStub() service = DataService(cache, default_underlying="GC=F") assert service.default_symbol == "GC=F" @pytest.mark.asyncio async def test_data_service_with_custom_underlying(self) -> None: """DataService can be initialized with custom underlying.""" cache = _CacheStub() service = DataService(cache, default_underlying="GC=F") assert service.default_underlying == "GC=F" @pytest.mark.asyncio async def test_get_quote_routes_gc_f_to_futures_method(self) -> None: """get_quote routes GC=F to _fetch_gc_futures.""" cache = _CacheStub() service = DataService(cache) # GC=F should return data with symbol GC=F # (source may be 'yfinance' if available, or 'fallback' if not) quote = await service.get_quote("GC=F") assert quote["symbol"] == "GC=F" assert quote["source"] in {"yfinance", "fallback"} assert "price" in quote @pytest.mark.asyncio async def test_get_option_expirations_gc_f_returns_placeholder(self) -> None: """get_option_expirations returns placeholder for GC=F.""" cache = _CacheStub() service = DataService(cache) # Mock a quote in cache to avoid network call await cache.set_json("quote:GC=F", {"symbol": "GC=F", "price": 2700.0, "source": "test"}) expirations = await service.get_option_expirations("GC=F") assert expirations["symbol"] == "GC=F" assert expirations["expirations"] == [] assert "coming soon" in expirations.get("error", "").lower() assert expirations["source"] == "placeholder" @pytest.mark.asyncio async def test_get_options_chain_for_expiry_gc_f_returns_placeholder(self) -> None: """get_options_chain_for_expiry returns placeholder for GC=F.""" cache = _CacheStub() service = DataService(cache) # Mock quote and expirations in cache await cache.set_json("quote:GC=F", {"symbol": "GC=F", "price": 2700.0, "source": "test"}) await cache.set_json( "options:GC=F:expirations", {"symbol": "GC=F", "expirations": ["2026-04-01"], "source": "test"} ) chain = await service.get_options_chain_for_expiry("GC=F", "2026-04-01") assert chain["symbol"] == "GC=F" assert chain["calls"] == [] assert chain["puts"] == [] assert chain["rows"] == [] assert "coming soon" in chain.get("error", "").lower() assert chain["source"] == "placeholder" @pytest.mark.asyncio async def test_get_option_expirations_gld_uses_yfinance_path(self) -> None: """get_option_expirations for GLD follows normal yfinance path.""" cache = _CacheStub() service = DataService(cache) # Without yfinance, should fall back expirations = await service.get_option_expirations("GLD") assert expirations["symbol"] == "GLD" # Should not be marked as "placeholder" (that's GC=F specific) assert expirations["source"] != "placeholder" @pytest.mark.asyncio async def test_supported_underlyings_in_domain(self) -> None: """Verify supported_underlyings is accessible from domain.""" from app.domain.instruments import Underlying, supported_underlyings underlyings = supported_underlyings() assert len(underlyings) == 2 assert Underlying.GLD in underlyings assert Underlying.GC_F in underlyings class TestPortfolioConfigUnderlying: """Test PortfolioConfig underlying field.""" def test_portfolio_config_has_underlying_field(self) -> None: """PortfolioConfig has underlying field defaulting to GLD.""" from app.models.portfolio import PortfolioConfig config = PortfolioConfig() assert hasattr(config, "underlying") assert config.underlying == "GLD" def test_portfolio_config_underlying_in_to_dict(self) -> None: """PortfolioConfig.to_dict includes underlying field.""" from app.models.portfolio import PortfolioConfig config = PortfolioConfig(underlying="GC=F") data = config.to_dict() assert "underlying" in data assert data["underlying"] == "GC=F" def test_portfolio_config_underlying_from_dict(self) -> None: """PortfolioConfig.from_dict preserves underlying field.""" from app.models.portfolio import PortfolioConfig data = { "gold_value": 215000.0, "entry_price": 2150.0, "gold_ounces": 100.0, "underlying": "GC=F", } config = PortfolioConfig.from_dict(data) assert config.underlying == "GC=F" def test_portfolio_config_underlying_persisted_fields(self) -> None: """PortfolioRepository includes underlying in persisted fields.""" from app.models.portfolio import PortfolioRepository assert "underlying" in PortfolioRepository._PERSISTED_FIELDS