"""Tests for CacheManager.""" import pytest from unittest.mock import AsyncMock, MagicMock, patch from exchange_data_manager.cache.manager import CacheManager from exchange_data_manager.config import DatabaseConfig from exchange_data_manager.candles.models import Candle, CandleRequest class MockConnector: """Mock exchange connector for testing.""" def __init__(self): self.fetch_candles = AsyncMock(return_value=[]) @pytest.fixture def cache_manager(): """Create a cache manager for testing (sync mode).""" return CacheManager(use_async_db=False) class TestCacheManagerColdCache: """Tests for cold-cache / limit-only requests.""" @pytest.mark.asyncio async def test_limit_only_request_fetches_from_exchange(self, cache_manager): """Test that limit-only requests (no start/end) fetch from exchange.""" # Setup mock connector mock_connector = MockConnector() expected_candles = [ Candle(time=1709337600, open=50000.0, high=50100.0, low=49900.0, close=50050.0, volume=10.0), Candle(time=1709337660, open=50050.0, high=50200.0, low=50000.0, close=50150.0, volume=15.0), ] mock_connector.fetch_candles.return_value = expected_candles cache_manager.register_exchange("binance", mock_connector) # Request with limit but no start/end (cold cache scenario) request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=None, end=None, limit=100, ) result = await cache_manager.get_candles(request) # Should have fetched from exchange mock_connector.fetch_candles.assert_called_once_with( symbol="BTC/USDT", timeframe="1m", limit=100, ) assert len(result) == 2 assert result[0].time == 1709337600 @pytest.mark.asyncio async def test_limit_only_no_connector_returns_empty(self, cache_manager): """Test limit-only request with no connector returns empty list.""" request = CandleRequest( exchange="unknown_exchange", symbol="BTC/USDT", timeframe="1m", start=None, end=None, limit=100, ) result = await cache_manager.get_candles(request) assert result == [] @pytest.mark.asyncio @patch("exchange_data_manager.cache.memory.time.time") async def test_limit_only_caches_result(self, mock_time, cache_manager): """Test that limit-only results are cached in memory.""" # Mock time to be close to test candle timestamps (prevents freshness check) mock_time.return_value = 1709337660 # Just after the candle mock_connector = MockConnector() expected_candles = [ Candle(time=1709337600, open=50000.0, high=50100.0, low=49900.0, close=50050.0, volume=10.0), ] mock_connector.fetch_candles.return_value = expected_candles cache_manager.register_exchange("binance", mock_connector) request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=None, end=None, limit=100, ) # First call - fetches from exchange result1 = await cache_manager.get_candles(request) assert mock_connector.fetch_candles.call_count == 1 assert len(result1) == 1 # Second call - memory cache has data, so doesn't need to fetch again result2 = await cache_manager.get_candles(request) assert mock_connector.fetch_candles.call_count == 1 # Still 1 - used cache assert len(result2) == 1 class TestCacheManagerStats: """Tests for cache manager stats.""" @pytest.mark.asyncio async def test_stats_returns_all_components(self, cache_manager): """Test that stats returns memory, database, and exchanges.""" stats = await cache_manager.stats() assert "memory" in stats assert "database" in stats assert "registered_exchanges" in stats assert isinstance(stats["registered_exchanges"], list) class TestCacheManagerSources: """Tests for source reporting and per-request connector overrides.""" @pytest.mark.asyncio @patch("exchange_data_manager.cache.memory.time.time") async def test_get_candles_with_source_reports_memory(self, mock_time, cache_manager): """Memory-only responses should report memory source.""" # Mock time to be close to test candle timestamps (prevents freshness check) mock_time.return_value = 1709337720 # Just after the second candle candles = [ Candle(time=1709337600, open=50000.0, high=50100.0, low=49900.0, close=50050.0, volume=10.0), Candle(time=1709337660, open=50050.0, high=50200.0, low=50000.0, close=50150.0, volume=15.0), ] cache_manager.memory.put("binance:BTC/USDT:1m", candles) request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", limit=2, ) result, source = await cache_manager.get_candles_with_source(request) assert len(result) == 2 assert source == "memory" @pytest.mark.asyncio async def test_get_candles_with_source_uses_connector_override(self, cache_manager): """A session-scoped connector override should be used when provided.""" override_connector = MockConnector() override_connector.fetch_candles.return_value = [ Candle(time=1709337600, open=50000.0, high=50100.0, low=49900.0, close=50050.0, volume=10.0), ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", limit=1, ) result, source = await cache_manager.get_candles_with_source( request, connector_override=override_connector, ) assert len(result) == 1 assert source == "exchange" override_connector.fetch_candles.assert_called_once_with( symbol="BTC/USDT", timeframe="1m", limit=1, ) class TestCacheManagerDatabaseConfig: """Tests for database config wiring.""" def test_async_database_pool_config_is_wired(self): """CacheManager should pass pool config into AsyncDatabaseCache.""" manager = CacheManager( database_config=DatabaseConfig( path="./data/test-pool.db", pool_size=7, max_overflow=3, ), use_async_db=True, ) assert manager.database._pool_size == 7 assert manager.database._max_overflow == 3 class TestCacheManagerExchangeRegistration: """Tests for exchange connector registration.""" def test_register_exchange(self, cache_manager): """Test registering an exchange connector.""" mock_connector = MockConnector() cache_manager.register_exchange("test_exchange", mock_connector) assert cache_manager.get_exchange("test_exchange") is mock_connector def test_get_exchange_case_insensitive(self, cache_manager): """Test that exchange lookup is case insensitive.""" mock_connector = MockConnector() cache_manager.register_exchange("TestExchange", mock_connector) assert cache_manager.get_exchange("testexchange") is mock_connector assert cache_manager.get_exchange("TESTEXCHANGE") is mock_connector def test_get_nonexistent_exchange_returns_none(self, cache_manager): """Test that getting a non-existent exchange returns None.""" result = cache_manager.get_exchange("nonexistent") assert result is None