import asyncio import unittest from unittest.mock import AsyncMock, Mock from datetime import datetime from data.collector.collector_callback_dispatcher import CallbackDispatcher from data.common.data_types import DataType, MarketDataPoint class TestCallbackDispatcher(unittest.IsolatedAsyncioTestCase): def setUp(self): self.mock_logger = Mock() self.dispatcher = CallbackDispatcher( component_name="test_dispatcher", logger=self.mock_logger ) async def test_init(self): self.assertEqual(self.dispatcher.component_name, "test_dispatcher") self.assertEqual(self.dispatcher.logger, self.mock_logger) self.assertIsInstance(self.dispatcher._data_callbacks, dict) self.assertGreater(len(self.dispatcher._data_callbacks), 0) # Ensure all DataType enums are initialized async def test_add_data_callback(self): mock_callback = Mock() data_type = DataType.CANDLE self.dispatcher.add_data_callback(data_type, mock_callback) self.assertIn(mock_callback, self.dispatcher._data_callbacks[data_type]) self.mock_logger.debug.assert_called_with(f"test_dispatcher: Added callback for {data_type.value} data") # Test adding same callback twice (should not add) self.dispatcher.add_data_callback(data_type, mock_callback) self.assertEqual(self.dispatcher._data_callbacks[data_type].count(mock_callback), 1) async def test_remove_data_callback(self): mock_callback = Mock() data_type = DataType.TRADE self.dispatcher.add_data_callback(data_type, mock_callback) self.assertIn(mock_callback, self.dispatcher._data_callbacks[data_type]) self.dispatcher.remove_data_callback(data_type, mock_callback) self.assertNotIn(mock_callback, self.dispatcher._data_callbacks[data_type]) self.mock_logger.debug.assert_called_with(f"test_dispatcher: Removed callback for {data_type.value} data") # Test removing non-existent callback (should do nothing) self.dispatcher.remove_data_callback(data_type, Mock()) # No error should be raised and log should not be called again for removal async def test_notify_callbacks_sync(self): mock_sync_callback = Mock() data_type = DataType.TICKER data_point = MarketDataPoint("exchange", "symbol", datetime.now(), data_type, {"price": 100}) self.dispatcher.add_data_callback(data_type, mock_sync_callback) await self.dispatcher.notify_callbacks(data_point) mock_sync_callback.assert_called_once_with(data_point) async def test_notify_callbacks_async(self): mock_async_callback = AsyncMock() data_type = DataType.ORDERBOOK data_point = MarketDataPoint("exchange", "symbol", datetime.now(), data_type, {"bids": [], "asks": []}) self.dispatcher.add_data_callback(data_type, mock_async_callback) await self.dispatcher.notify_callbacks(data_point) mock_async_callback.assert_called_once_with(data_point) async def test_notify_callbacks_mixed(self): mock_sync_callback = Mock() mock_async_callback = AsyncMock() data_type = DataType.BALANCE data_point = MarketDataPoint("exchange", "symbol", datetime.now(), data_type, {"asset": "BTC", "balance": 0.5}) self.dispatcher.add_data_callback(data_type, mock_sync_callback) self.dispatcher.add_data_callback(data_type, mock_async_callback) await self.dispatcher.notify_callbacks(data_point) mock_sync_callback.assert_called_once_with(data_point) mock_async_callback.assert_called_once_with(data_point) async def test_notify_callbacks_exception_handling(self): def failing_sync_callback(data): raise ValueError("Sync error") async def failing_async_callback(data): raise TypeError("Async error") mock_successful_callback = Mock() data_type = DataType.CANDLE data_point = MarketDataPoint("exchange", "symbol", datetime.now(), data_type, {}) self.dispatcher.add_data_callback(data_type, failing_sync_callback) self.dispatcher.add_data_callback(data_type, failing_async_callback) self.dispatcher.add_data_callback(data_type, mock_successful_callback) await self.dispatcher.notify_callbacks(data_point) mock_successful_callback.assert_called_once_with(data_point) self.assertEqual(self.mock_logger.error.call_count, 2) self.mock_logger.error.assert_any_call(f"test_dispatcher: Error in data callback for {data_type.value} {data_point.symbol}: Sync error", exc_info=True) self.mock_logger.error.assert_any_call(f"test_dispatcher: Error in data callback for {data_type.value} {data_point.symbol}: Async error", exc_info=True) async def test_notify_callbacks_no_callbacks(self): data_type = DataType.TICKER data_point = MarketDataPoint("exchange", "symbol", datetime.now(), data_type, {}) # No callbacks added await self.dispatcher.notify_callbacks(data_point) self.mock_logger.error.assert_not_called() # No errors should be logged self.mock_logger.debug.assert_not_called() # No debug logs from notify