class TestKucoinAPIUserStreamDataSource(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_key = "someKey" cls.api_passphrase = "somePassPhrase" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.auth = KucoinAuth(self.api_key, self.api_passphrase, self.api_secret_key) self.data_source = KucoinAPIUserStreamDataSource( self.throttler, self.auth) self.mocking_assistant = NetworkMockingAssistant() def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @staticmethod def get_listen_key_mock(): listen_key = { "code": "200000", "data": { "token": "someToken", "instanceServers": [{ "endpoint": "wss://someEndpoint", "encrypt": True, "protocol": "websocket", "pingInterval": 18000, "pingTimeout": 10000, }] } } return listen_key @aioresponses() def test_get_listen_key_raises(self, mock_api): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL mock_api.post(url, status=500) with self.assertRaises(IOError): self.async_run_with_timeout(self.data_source.get_listen_key()) @aioresponses() def test_get_listen_key(self, mock_api): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL resp = self.get_listen_key_mock() mock_api.post(url, body=json.dumps(resp)) ret = self.async_run_with_timeout(self.data_source.get_listen_key()) self.assertEqual(ret, resp) # shallow comparison ok @aioresponses() @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_to_user_stream_subscribes_to_private_topics( self, mock_api, ws_connect_mock): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL resp = self.get_listen_key_mock() mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) msg_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(len(CONSTANTS.PRIVATE_ENDPOINT_NAMES), len(sent_messages)) subscribed_endpoints = {m["topic"] for m in sent_messages} self.assertEqual(set(CONSTANTS.PRIVATE_ENDPOINT_NAMES), subscribed_endpoints) @aioresponses() @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_to_user_stream_accepts_message(self, mock_api, ws_connect_mock): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL resp = self.get_listen_key_mock() mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) msg = "someMsg" msg_queue = asyncio.Queue() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(msg)) self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue(not msg_queue.empty()) queued = msg_queue.get_nowait() self.assertEqual(msg, queued) @aioresponses() @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_to_user_stream_sends_ping_ignores_pong( self, mock_api, ws_connect_mock): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL resp = self.get_listen_key_mock() mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.PONG) msg = "someMsg" self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(msg)) msg_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) ws_connect_mock.return_value.ping.assert_called() # ping was sent self.assertTrue(not msg_queue.empty()) queued = msg_queue.get_nowait() self.assertEqual(msg, queued)
class BinanceAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = cls.base_asset + cls.quote_asset cls.domain = "com" def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.mocking_assistant = NetworkMockingAssistant() self.time_synchronizer = TimeSynchronizer() self.time_synchronizer.add_time_offset_ms_sample(1000) self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = BinanceAPIOrderBookDataSource( trading_pairs=[self.trading_pair], throttler=self.throttler, domain=self.domain, time_synchronizer=self.time_synchronizer) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.resume_test_event = asyncio.Event() BinanceAPIOrderBookDataSource._trading_pair_symbol_map = { "com": bidict({f"{self.base_asset}{self.quote_asset}": self.trading_pair}) } def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() BinanceAPIOrderBookDataSource._trading_pair_symbol_map = {} super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _successfully_subscribed_event(self): resp = {"result": None, "id": 1} return resp def _trade_update_event(self): resp = { "e": "trade", "E": 123456789, "s": self.ex_trading_pair, "t": 12345, "p": "0.001", "q": "100", "b": 88, "a": 50, "T": 123456785, "m": True, "M": True } return resp def _order_diff_event(self): resp = { "e": "depthUpdate", "E": 123456789, "s": self.ex_trading_pair, "U": 157, "u": 160, "b": [["0.0024", "10"]], "a": [["0.0026", "100"]] } return resp def _snapshot_response(self): resp = { "lastUpdateId": 1027024, "bids": [["4.00000000", "431.00000000"]], "asks": [["4.00000200", "12.00000000"]] } return resp @aioresponses() def test_get_last_trade_prices(self, mock_api): url = web_utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.domain) url = f"{url}?symbol={self.base_asset}{self.quote_asset}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "symbol": "BNBBTC", "priceChange": "-94.99999800", "priceChangePercent": "-95.960", "weightedAvgPrice": "0.29628482", "prevClosePrice": "0.10002000", "lastPrice": "100.0", "lastQty": "200.00000000", "bidPrice": "4.00000000", "bidQty": "100.00000000", "askPrice": "4.00000200", "askQty": "100.00000000", "openPrice": "99.00000000", "highPrice": "100.00000000", "lowPrice": "0.10000000", "volume": "8913.30000000", "quoteVolume": "15.30000000", "openTime": 1499783499040, "closeTime": 1499869899040, "firstId": 28385, "lastId": 28460, "count": 76, } mock_api.get(regex_url, body=json.dumps(mock_response)) result: Dict[str, float] = self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler, time_synchronizer=self.time_synchronizer)) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair]) @aioresponses() def test_get_all_mid_prices(self, mock_api): url = web_utils.public_rest_url(CONSTANTS.SERVER_TIME_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) response = {"serverTime": 1640000003000} mock_api.get(regex_url, body=json.dumps(response)) url = web_utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.domain) mock_response: List[Dict[str, Any]] = [ { # Truncated Response "symbol": self.ex_trading_pair, "bidPrice": "99", "askPrice": "101", }, { # Truncated Response for unrecognized pair "symbol": "BCCBTC", "bidPrice": "99", "askPrice": "101", } ] mock_api.get(url, body=json.dumps(mock_response)) result: Dict[str, float] = self.async_run_with_timeout( self.data_source.get_all_mid_prices()) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair]) @aioresponses() def test_fetch_trading_pairs(self, mock_api): BinanceAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.public_rest_url( path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) mock_response: Dict[str, Any] = { "timezone": "UTC", "serverTime": 1639598493658, "rateLimits": [], "exchangeFilters": [], "symbols": [ { "symbol": "ETHBTC", "status": "TRADING", "baseAsset": "ETH", "baseAssetPrecision": 8, "quoteAsset": "BTC", "quotePrecision": 8, "quoteAssetPrecision": 8, "baseCommissionPrecision": 8, "quoteCommissionPrecision": 8, "orderTypes": [ "LIMIT", "LIMIT_MAKER", "MARKET", "STOP_LOSS_LIMIT", "TAKE_PROFIT_LIMIT" ], "icebergAllowed": True, "ocoAllowed": True, "quoteOrderQtyMarketAllowed": True, "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], "permissions": ["SPOT", "MARGIN"] }, { "symbol": "LTCBTC", "status": "TRADING", "baseAsset": "LTC", "baseAssetPrecision": 8, "quoteAsset": "BTC", "quotePrecision": 8, "quoteAssetPrecision": 8, "baseCommissionPrecision": 8, "quoteCommissionPrecision": 8, "orderTypes": [ "LIMIT", "LIMIT_MAKER", "MARKET", "STOP_LOSS_LIMIT", "TAKE_PROFIT_LIMIT" ], "icebergAllowed": True, "ocoAllowed": True, "quoteOrderQtyMarketAllowed": True, "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], "permissions": ["SPOT", "MARGIN"] }, { "symbol": "BNBBTC", "status": "TRADING", "baseAsset": "BNB", "baseAssetPrecision": 8, "quoteAsset": "BTC", "quotePrecision": 8, "quoteAssetPrecision": 8, "baseCommissionPrecision": 8, "quoteCommissionPrecision": 8, "orderTypes": [ "LIMIT", "LIMIT_MAKER", "MARKET", "STOP_LOSS_LIMIT", "TAKE_PROFIT_LIMIT" ], "icebergAllowed": True, "ocoAllowed": True, "quoteOrderQtyMarketAllowed": True, "isSpotTradingAllowed": True, "isMarginTradingAllowed": True, "filters": [], "permissions": ["MARGIN"] }, ] } mock_api.get(url, body=json.dumps(mock_response)) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs( time_synchronizer=self.time_synchronizer)) self.assertEqual(2, len(result)) self.assertIn("ETH-BTC", result) self.assertIn("LTC-BTC", result) self.assertNotIn("BNB-BTC", result) @aioresponses() def test_fetch_trading_pairs_exception_raised(self, mock_api): BinanceAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.public_rest_url( path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs( time_synchronizer=self.time_synchronizer)) self.assertEqual(0, len(result)) @aioresponses() def test_get_snapshot_successful(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) result: Dict[str, Any] = self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) self.assertEqual(self._snapshot_response(), result) @aioresponses() def test_get_snapshot_catch_exception(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) @aioresponses() def test_get_new_order_book(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { "lastUpdateId": 1, "bids": [["4.00000000", "431.00000000"]], "asks": [["4.00000200", "12.00000000"]] } mock_api.get(regex_url, body=json.dumps(mock_response)) result: OrderBook = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertEqual(1, result.snapshot_uid) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = {"result": None, "id": 1} result_subscribe_diffs = {"result": None, "id": 2} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_subscription_messages)) expected_trade_subscription = { "method": "SUBSCRIBE", "params": [f"{self.ex_trading_pair.lower()}@trade"], "id": 1 } self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) expected_diff_subscription = { "method": "SUBSCRIBE", "params": [f"{self.ex_trading_pair.lower()}@depth@100ms"], "id": 2 } self.assertEqual(expected_diff_subscription, sent_subscription_messages[1]) self.assertTrue( self._is_logged( "INFO", "Subscribed to public order book and trade channels...")) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect") def test_listen_for_subscriptions_raises_cancel_exception( self, mock_ws, _: AsyncMock): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_logs_exception_details( self, mock_ws, sleep_mock): mock_ws.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) def test_subscribe_channels_raises_cancel_exception(self): mock_ws = MagicMock() mock_ws.send.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) def test_subscribe_channels_raises_exception_and_logs_error(self): mock_ws = MagicMock() mock_ws.send.side_effect = Exception("Test Error") with self.assertRaises(Exception): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) def test_listen_for_trades_cancelled_when_listening(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_trades_logs_exception(self): incomplete_resp = { "m": 1, "i": 2, } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public trade updates from exchange" )) def test_listen_for_trades_successful(self): mock_queue = AsyncMock() mock_queue.get.side_effect = [ self._trade_update_event(), asyncio.CancelledError() ] self.data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(12345, msg.trade_id) def test_listen_for_order_book_diffs_cancelled(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_order_book_diffs_logs_exception(self): incomplete_resp = { "m": 1, "i": 2, } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public order book updates from exchange" )) def test_listen_for_order_book_diffs_successful(self): mock_queue = AsyncMock() mock_queue.get.side_effect = [ self._order_diff_event(), asyncio.CancelledError() ] self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(12345, msg.update_id) @aioresponses() def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot( self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_order_book_snapshots( self.ev_loop, asyncio.Queue())) @aioresponses() @patch( "hummingbot.connector.exchange.binance.binance_api_order_book_data_source" ".BinanceAPIOrderBookDataSource._sleep") def test_listen_for_order_book_snapshots_log_exception( self, mock_api, sleep_mock): msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}." )) @aioresponses() def test_listen_for_order_book_snapshots_successful( self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() url = web_utils.public_rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(1027024, msg.update_id)
class TestKucoinAPIOrderBookDataSource(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ws_endpoint = "ws://someEndpoint" cls.api_key = "someKey" cls.api_passphrase = "somePassPhrase" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.log_records = [] self.async_task = None self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.time_synchronnizer = TimeSynchronizer() self.time_synchronnizer.add_time_offset_ms_sample(1000) self.ob_data_source = KucoinAPIOrderBookDataSource( trading_pairs=[self.trading_pair], throttler=self.throttler, time_synchronizer=self.time_synchronnizer) self.ob_data_source.logger().setLevel(1) self.ob_data_source.logger().addHandler(self) KucoinAPIOrderBookDataSource._trading_pair_symbol_map = { CONSTANTS.DEFAULT_DOMAIN: bidict({self.trading_pair: self.trading_pair}) } def tearDown(self) -> None: self.async_task and self.async_task.cancel() KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {} super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @staticmethod def get_snapshot_mock() -> Dict: snapshot = { "code": "200000", "data": { "time": 1630556205455, "sequence": "1630556205456", "bids": [["0.3003", "4146.5645"]], "asks": [["0.3004", "1553.6412"]] } } return snapshot @aioresponses() def test_get_last_traded_prices(self, mock_api): KucoinAPIOrderBookDataSource._trading_pair_symbol_map[ CONSTANTS.DEFAULT_DOMAIN]["TKN1-TKN2"] = "TKN1-TKN2" url1 = web_utils.rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=CONSTANTS.DEFAULT_DOMAIN) url1 = f"{url1}?symbol={self.trading_pair}" regex_url = re.compile(f"^{url1}".replace(".", r"\.").replace("?", r"\?")) resp = { "code": "200000", "data": { "sequence": "1550467636704", "bestAsk": "0.03715004", "size": "0.17", "price": "100", "bestBidSize": "3.803", "bestBid": "0.03710768", "bestAskSize": "1.788", "time": 1550653727731 } } mock_api.get(regex_url, body=json.dumps(resp)) url2 = web_utils.rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=CONSTANTS.DEFAULT_DOMAIN) url2 = f"{url2}?symbol=TKN1-TKN2" regex_url = re.compile(f"^{url2}".replace(".", r"\.").replace("?", r"\?")) resp = { "code": "200000", "data": { "sequence": "1550467636704", "bestAsk": "0.03715004", "size": "0.17", "price": "200", "bestBidSize": "3.803", "bestBid": "0.03710768", "bestAskSize": "1.788", "time": 1550653727731 } } mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=KucoinAPIOrderBookDataSource.get_last_traded_prices( [self.trading_pair, "TKN1-TKN2"])) ticker_requests = [(key, value) for key, value in mock_api.requests.items() if key[1].human_repr().startswith(url1) or key[1].human_repr().startswith(url2)] request_params = ticker_requests[0][1][0].kwargs["params"] self.assertEqual(f"{self.base_asset}-{self.quote_asset}", request_params["symbol"]) request_params = ticker_requests[1][1][0].kwargs["params"] self.assertEqual("TKN1-TKN2", request_params["symbol"]) self.assertEqual(ret[self.trading_pair], 100) self.assertEqual(ret["TKN1-TKN2"], 200) @aioresponses() def test_fetch_trading_pairs(self, mock_api): KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL) resp = { "data": [{ "symbol": self.trading_pair, "name": self.trading_pair, "baseCurrency": self.base_asset, "quoteCurrency": self.quote_asset, "enableTrading": True, }, { "symbol": "SOME-PAIR", "name": "SOME-PAIR", "baseCurrency": "SOME", "quoteCurrency": "PAIR", "enableTrading": False, }] } mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=KucoinAPIOrderBookDataSource.fetch_trading_pairs( throttler=self.throttler, time_synchronizer=self.time_synchronnizer, )) self.assertEqual(1, len(ret)) self.assertEqual(self.trading_pair, ret[0]) @aioresponses() def test_fetch_trading_pairs_exception_raised(self, mock_api): KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) result: Dict[str] = self.async_run_with_timeout( self.ob_data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) @aioresponses() def test_get_snapshot_raises(self, mock_api): url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=500) with self.assertRaises(IOError): self.async_run_with_timeout( coroutine=self.ob_data_source.get_snapshot(self.trading_pair)) @aioresponses() def test_get_snapshot(self, mock_api): url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_snapshot_mock() mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.ob_data_source.get_snapshot(self.trading_pair)) self.assertEqual(ret, resp) # shallow comparison ok @aioresponses() def test_get_new_order_book(self, mock_api): url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_snapshot_mock() mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.ob_data_source.get_new_order_book( self.trading_pair)) bid_entries = list(ret.bid_entries()) ask_entries = list(ret.ask_entries()) self.assertEqual(1, len(bid_entries)) self.assertEqual(0.3003, bid_entries[0].price) self.assertEqual(4146.5645, bid_entries[0].amount) self.assertEqual(int(resp["data"]["sequence"]), bid_entries[0].update_id) self.assertEqual(1, len(ask_entries)) self.assertEqual(0.3004, ask_entries[0].price) self.assertEqual(1553.6412, ask_entries[0].amount) self.assertEqual(int(resp["data"]["sequence"]), ask_entries[0].update_id) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id" ) def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs( self, mock_api, id_mock, ws_connect_mock): id_mock.side_effect = [1, 2] url = web_utils.rest_url(path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [{ "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 50000, "pingTimeout": 10000 }], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = {"type": "ack", "id": 1} result_subscribe_diffs = {"type": "ack", "id": 2} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_subscription_messages)) expected_trade_subscription = { "id": 1, "type": "subscribe", "topic": f"/market/match:{self.trading_pair}", "privateChannel": False, "response": False } self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) expected_diff_subscription = { "id": 2, "type": "subscribe", "topic": f"/market/level2:{self.trading_pair}", "privateChannel": False, "response": False } self.assertEqual(expected_diff_subscription, sent_subscription_messages[1]) self.assertTrue( self._is_logged( "INFO", "Subscribed to public order book and trade channels...")) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id" ) @patch( "hummingbot.connector.exchange.kucoin.kucoin_api_order_book_data_source.KucoinAPIOrderBookDataSource._time" ) def test_listen_for_subscriptions_sends_ping_message_before_ping_interval_finishes( self, mock_api, time_mock, id_mock, ws_connect_mock): id_mock.side_effect = [1, 2, 3, 4] time_mock.side_effect = [ 1000, 1100, 1101, 1102 ] # Simulate first ping interval is already due url = web_utils.rest_url(path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [{ "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 20000, "pingTimeout": 10000 }], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = {"type": "ack", "id": 1} result_subscribe_diffs = {"type": "ack", "id": 2} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) expected_ping_message = { "id": 3, "type": "ping", } self.assertEqual(expected_ping_message, sent_messages[-1]) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) def test_listen_for_subscriptions_raises_cancel_exception( self, mock_api, _, ws_connect_mock): url = web_utils.rest_url(path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [{ "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 50000, "pingTimeout": 10000 }], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) def test_listen_for_subscriptions_logs_exception_details( self, mock_api, sleep_mock, ws_connect_mock): url = web_utils.rest_url(path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [{ "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 50000, "pingTimeout": 10000 }], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) sleep_mock.side_effect = asyncio.CancelledError ws_connect_mock.side_effect = Exception("TEST ERROR.") with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) def test_listen_for_trades_cancelled_when_listening(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError() self.ob_data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_trades_logs_exception(self): incomplete_resp = { "type": "message", "topic": f"/market/match:{self.trading_pair}", } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.ob_data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public trade updates from exchange" )) def test_listen_for_trades_successful(self): mock_queue = AsyncMock() trade_event = { "type": "message", "topic": f"/market/match:{self.trading_pair}", "subject": "trade.l3match", "data": { "sequence": "1545896669145", "type": "match", "symbol": self.trading_pair, "side": "buy", "price": "0.08200000000000000000", "size": "0.01022222000000000000", "tradeId": "5c24c5da03aa673885cd67aa", "takerOrderId": "5c24c5d903aa6772d55b371e", "makerOrderId": "5c2187d003aa677bd09d5c93", "time": "1545913818099033203" } } mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()] self.ob_data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(trade_event["data"]["tradeId"], msg.trade_id) def test_listen_for_order_book_diffs_cancelled(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.ob_data_source._message_queue[ CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_order_book_diffs_logs_exception(self): incomplete_resp = { "type": "message", "topic": f"/market/level2:{self.trading_pair}", } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.ob_data_source._message_queue[ CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public order book updates from exchange" )) def test_listen_for_order_book_diffs_successful(self): mock_queue = AsyncMock() diff_event = { "type": "message", "topic": "/market/level2:BTC-USDT", "subject": "trade.l2update", "data": { "sequenceStart": 1545896669105, "sequenceEnd": 1545896669106, "symbol": f"{self.trading_pair}", "changes": { "asks": [["6", "1", "1545896669105"]], "bids": [["4", "1", "1545896669106"]] } } } mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()] self.ob_data_source._message_queue[ CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(diff_event["data"]["sequenceEnd"], msg.update_id) @aioresponses() def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot( self, mock_api): url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.ob_data_source.listen_for_order_book_snapshots( self.ev_loop, asyncio.Queue())) @aioresponses() @patch( "hummingbot.connector.exchange.kucoin.kucoin_api_order_book_data_source" ".KucoinAPIOrderBookDataSource._sleep") def test_listen_for_order_book_snapshots_log_exception( self, mock_api, sleep_mock): msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = asyncio.CancelledError url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) try: self.async_run_with_timeout( self.ob_data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}." )) @aioresponses() def test_listen_for_order_book_snapshots_successful( self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) snapshot_data = { "code": "200000", "data": { "sequence": "3262786978", "time": 1550653727731, "bids": [["6500.12", "0.45054140"], ["6500.11", "0.45054140"]], "asks": [["6500.16", "0.57753524"], ["6500.15", "0.57753524"]] } } mock_api.get(regex_url, body=json.dumps(snapshot_data)) self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(int(snapshot_data["data"]["sequence"]), msg.update_id)
class BitmartAPIUserStreamDataSourceTests(unittest.TestCase): # the level is required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.api_key = 'testAPIKey' cls.secret = 'testSecret' cls.memo = '001' cls.account_id = 528 cls.username = '******' cls.oms_id = 1 cls.ev_loop = asyncio.get_event_loop() def setUp(self) -> None: super().setUp() self.listening_task = None self.log_records = [] throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) auth_assistant = BitmartAuth(api_key=self.api_key, secret_key=self.secret, memo=self.memo) self.data_source = BitmartAPIUserStreamDataSource( auth_assistant, throttler) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.data_source._trading_pairs = ["HBOT-USDT"] self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _raise_exception(self, exception_class): raise exception_class @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_authenticates_and_subscribes_to_events( self, ws_connect_mock): mock_response: Dict[Any] = { "data": [{ "symbol": "BTC_USDT", "side": "buy", "type": "market", "notional": "", "size": "1.0000000000", "ms_t": "1609926028000", "price": "46100.0000000000", "filled_notional": "46100.0000000000", "filled_size": "1.0000000000", "margin_trading": "0", "state": "2", "order_id": "2147857398", "order_type": "0", "last_fill_time": "1609926039226", "last_fill_price": "46100.00000", "last_fill_count": "1.00000" }], "table": "spot/user/order" } messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) initial_last_recv_time = self.data_source.last_recv_time self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(mock_response)) first_received_message = self.ev_loop.run_until_complete( messages.get()) self.assertEqual(mock_response, first_received_message) self.assertTrue( self._is_logged('INFO', "Authenticating to User Stream...")) self.assertTrue( self._is_logged('INFO', "Successfully authenticated to User Stream.")) self.assertTrue( self._is_logged( 'INFO', "Successfully subscribed to all Private channels.")) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(2, len(sent_messages)) auth_req = sent_messages[0] sub_req = sent_messages[1] self.assertTrue("op" in auth_req and "args" in auth_req and "testAPIKey" in auth_req["args"]) self.assertEqual( { "op": "subscribe", "args": ["spot/user/order:HBOT_USDT"] }, sub_req) self.assertGreater(self.data_source.last_recv_time, initial_last_recv_time) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_fails_when_authentication_fails( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.closed = False ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({ "errorCode": "test code", "errorMessage": "test err message" })) try: self.ev_loop.run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "WebSocket login errored with message: test err message")) self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream.")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with BitMart WebSocket connection. " "Retrying after 30 seconds...")) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_initialization( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream( self.ev_loop, messages)) self.ev_loop.run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if "testAPIKey" in sent_message["args"] else self.mocking_assistant ._sent_websocket_json_messages[ws_connect_mock.return_value ].append(sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream( self.ev_loop, messages)) self.ev_loop.run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_events_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if "spot/user/order:HBOT_USDT" in sent_message["args"] else self. mocking_assistant._sent_websocket_json_messages[ ws_connect_mock.return_value].append(sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream( self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) self.ev_loop.run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_initialization( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.side_effect = Exception with self.assertRaises(Exception): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream( self.ev_loop, messages)) try: self.async_run_with_timeout(self.listening_task) except asyncio.TimeoutError: raise self.assertTrue( self._is_logged( "ERROR", "Unexpected error with BitMart WebSocket connection. Retrying after 30 seconds..." )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if "testAPIKey" in sent_message["args"] else self.mocking_assistant ._sent_websocket_json_messages[ws_connect_mock.return_value ].append(sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.closed = False ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream( self.ev_loop, messages)) self.ev_loop.run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream.")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with BitMart WebSocket connection. " "Retrying after 30 seconds...")) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_events_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if "spot/user/order:HBOT_USDT" in sent_message["args"] else self. mocking_assistant._sent_websocket_json_messages[ ws_connect_mock.return_value].append(sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.closed = False ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream( self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) self.async_run_with_timeout(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occured during subscribing to Bitmart private channels." )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with BitMart WebSocket connection. " "Retrying after 30 seconds..."))
class HuobiAPIUserStreamDataSourceTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.auth = HuobiAuth("somKey", "someSecretKey") cls.ev_loop = asyncio.get_event_loop() def setUp(self) -> None: super().setUp() self.log_records = [] self.async_tasks: List[asyncio.Task] = [] self.api_factory = build_api_factory() self.data_source = HuobiAPIUserStreamDataSource( huobi_auth=self.auth, api_factory=self.api_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() self.resume_test_event = asyncio.Event() def tearDown(self) -> None: for task in self.async_tasks: task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def test_get_ws_assistant(self): data_source = HuobiAPIUserStreamDataSource(self.auth) self.assertIsNone(data_source._ws_assistant) initial_ws_assistant = self.async_run_with_timeout( data_source._get_ws_assistant()) self.assertIsNotNone(data_source._ws_assistant) self.assertIsInstance(initial_ws_assistant, WSAssistant) subsequent_ws_assistant = self.async_run_with_timeout( data_source._get_ws_assistant()) self.assertEqual(initial_ws_assistant, subsequent_ws_assistant) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_authenticate_client_raises_cancelled(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError # Initialise WSAssistant and assume connected to websocket server self.async_run_with_timeout(self.data_source._get_ws_assistant()) self.async_run_with_timeout( self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL)) self.assertIsNotNone(self.data_source._ws_assistant) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source._authenticate_client()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_authenticate_client_logs_exception(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Initialise WSAssistant and assume connected to websocket server self.async_run_with_timeout(self.data_source._get_ws_assistant()) self.async_run_with_timeout( self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL)) self.assertIsNotNone(self.data_source._ws_assistant) ws_connect_mock.return_value.send_json.side_effect = Exception( "TEST ERROR") with self.assertRaisesRegex(Exception, "TEST ERROR"): self.async_run_with_timeout( self.data_source._authenticate_client()) self._is_logged( "ERROR", "Error occurred authenticating websocket connection... Error: TEST ERROR" ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_authenticate_client_failed(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Initialise WSAssistant and assume connected to websocket server self.async_run_with_timeout(self.data_source._get_ws_assistant()) self.async_run_with_timeout( self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL)) self.assertIsNotNone(self.data_source._ws_assistant) error_auth_response = { "action": "req", "code": 0, "TEST_ERROR": "ERROR WITH AUTHENTICATION" } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(error_auth_response)) with self.assertRaisesRegex(ValueError, "User Stream Authentication Fail!"): self.async_run_with_timeout( self.data_source._authenticate_client()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_authenticate_client_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Initialise WSAssistant and assume connected to websocket server self.async_run_with_timeout(self.data_source._get_ws_assistant()) self.async_run_with_timeout( self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL)) self.assertIsNotNone(self.data_source._ws_assistant) successful_auth_response = { "action": "req", "code": 200, "ch": "auth", "data": {} } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_auth_response)) result = self.async_run_with_timeout( self.data_source._authenticate_client()) self.assertIsNone(result) self._is_logged("INFO", "Successfully authenticated to user...") @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_subscribe_channels_raises_cancelled(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError # Initialise WSAssistant and assume connected to websocket server self.async_run_with_timeout(self.data_source._get_ws_assistant()) self.async_run_with_timeout( self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL)) self.assertIsNotNone(self.data_source._ws_assistant) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout(self.data_source._subscribe_channels()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_subscribe_channels_subscribe_topic_fail(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Initialise WSAssistant and assume connected to websocket server self.async_run_with_timeout(self.data_source._get_ws_assistant()) self.async_run_with_timeout( self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL)) self.assertIsNotNone(self.data_source._ws_assistant) error_sub_response = { "action": "sub", "code": 0, "TEST_ERROR": "ERROR SUBSCRIBING TO USER STREAM TOPIC" } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(error_sub_response)) with self.assertRaisesRegex(ValueError, "Error subscribing to topic: "): self.async_run_with_timeout(self.data_source._subscribe_channels()) self._is_logged( "ERROR", f"Cannot subscribe to user stream topic: {CONSTANTS.HUOBI_ORDER_UPDATE_TOPIC}" ) self._is_logged( "ERROR", "Unexpected error occurred subscribing to private user streams...") @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_subscribe_channels_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Initialise WSAssistant and assume connected to websocket server self.async_run_with_timeout(self.data_source._get_ws_assistant()) self.async_run_with_timeout( self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL)) self.assertIsNotNone(self.data_source._ws_assistant) successful_sub_trades_response = { "action": "sub", "code": 200, "ch": "trade.clearing#*", "data": {} } successful_sub_order_response = { "action": "sub", "code": 200, "ch": "orders#*", "data": {} } successful_sub_account_response = { "action": "sub", "code": 200, "ch": "accounts.update#2", "data": {} } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_trades_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_order_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_account_response)) result = self.async_run_with_timeout( self.data_source._subscribe_channels()) self.assertIsNone(result) subscription_requests_sent = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) expected_orders_channel_subscription = { "action": "sub", "ch": "orders#*" } self.assertIn(expected_orders_channel_subscription, subscription_requests_sent) expected_accounts_channel_subscription = { "action": "sub", "ch": "accounts.update#2" } self.assertIn(expected_accounts_channel_subscription, subscription_requests_sent) expected_trades_channel_subscription = { "action": "sub", "ch": "trade.clearing#*" } self.assertIn(expected_trades_channel_subscription, subscription_requests_sent) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep" ) def test_listen_for_user_stream_raises_cancelled_error( self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.side_effect = asyncio.CancelledError msg_queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_user_stream(msg_queue)) self.assertEqual(0, msg_queue.qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep" ) def test_listen_for_user_stream_logs_exception(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) successful_auth_response = { "action": "req", "code": 200, "ch": "auth", "data": {} } successful_sub_trades_response = { "action": "sub", "code": 200, "ch": "trade.clearing#*", "data": {} } successful_sub_order_response = { "action": "sub", "code": 200, "ch": "orders#*", "data": {} } successful_sub_account_response = { "action": "sub", "code": 200, "ch": "accounts.update#2", "data": {} } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_auth_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_trades_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_order_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_account_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.CLOSE) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(0, msg_queue.qsize()) self._is_logged( "ERROR", "Unexpected error with Huobi WebSocket connection. Retrying after 30 seconds..." ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep" ) def test_listen_for_user_stream_handle_ping(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) successful_auth_response = { "action": "req", "code": 200, "ch": "auth", "data": {} } successful_sub_trades_response = { "action": "sub", "code": 200, "ch": "trade.clearing#*", "data": {} } successful_sub_order_response = { "action": "sub", "code": 200, "ch": "orders#*", "data": {} } successful_sub_account_response = { "action": "sub", "code": 200, "ch": "accounts.update#2", "data": {} } ping_response = {"action": "ping", "data": {"ts": 1637553193021}} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_auth_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_trades_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_order_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_account_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(ping_response)) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(0, msg_queue.qsize()) sent_json = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertTrue(any(["pong" in str(payload) for payload in sent_json])) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep" ) def test_listen_for_user_stream_enqueues_updates(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) successful_auth_response = { "action": "req", "code": 200, "ch": "auth", "data": {} } successful_sub_trades_response = { "action": "sub", "code": 200, "ch": "trade.clearing#*", "data": {} } successful_sub_order_response = { "action": "sub", "code": 200, "ch": "orders#*", "data": {} } successful_sub_account_response = { "action": "sub", "code": 200, "ch": "accounts.update#2", "data": {} } ping_response = {"action": "ping", "data": {"ts": 1637553193021}} order_update_response = { "action": "push", "ch": "orders#", "data": { "execAmt": "0", "lastActTime": 1637553210074, "orderSource": "spot-api", "remainAmt": "0.005", "orderPrice": "4122.62", "orderSize": "0.005", "symbol": "ethusdt", "orderId": 414497810678464, "orderStatus": "canceled", "eventType": "cancellation", "clientOrderId": "AAc484720a-buy-ETH-USDT-1637553180003697", "type": "buy-limit-maker", }, } account_update_response = { "action": "push", "ch": "accounts.update#2", "data": { "currency": "usdt", "accountId": 15026496, "balance": "100", "available": "100", "changeType": "order.cancel", "accountType": "trade", "seqNum": 117, "changeTime": 1637553210076, }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_auth_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_trades_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_order_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(successful_sub_account_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(ping_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(order_update_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(account_update_response)) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(2, msg_queue.qsize())
class WSConnectionTest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.ws_url = "ws://some/url" def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() self.client_session = aiohttp.ClientSession() self.ws_connection = WSConnection(self.client_session) self.async_tasks: List[asyncio.Task] = [] def tearDown(self) -> None: self.ws_connection.disconnect() self.client_session.close() for task in self.async_tasks: task.cancel() super().tearDown() def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_connect_and_disconnect(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.assertFalse(self.ws_connection.connected) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.assertTrue(self.ws_connection.connected) self.async_run_with_timeout(self.ws_connection.disconnect()) self.assertFalse(self.ws_connection.connected) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_attempt_to_connect_second_time_raises(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) with self.assertRaises(RuntimeError) as e: self.async_run_with_timeout(self.ws_connection.connect( self.ws_url)) self.assertEqual("WS is connected.", str(e.exception)) def test_send_when_disconnected_raises(self): request = WSRequest(payload={"one": 1}) with self.assertRaises(RuntimeError) as e: self.async_run_with_timeout(self.ws_connection.send(request)) self.assertEqual("WS is not connected.", str(e.exception)) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_send(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) request = WSRequest(payload={"one": 1}) self.async_run_with_timeout(self.ws_connection.send(request)) json_msgs = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(1, len(json_msgs)) self.assertEqual(request.payload, json_msgs[0]) def test_receive_when_disconnected_raises(self): with self.assertRaises(RuntimeError) as e: self.async_run_with_timeout(self.ws_connection.receive()) self.assertEqual("WS is not connected.", str(e.exception)) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_raises_on_timeout(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) def raise_timeout(*_, **__): raise asyncio.TimeoutError ws_connect_mock.return_value.receive.side_effect = raise_timeout self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) with self.assertRaises(asyncio.TimeoutError) as e: self.async_run_with_timeout(self.ws_connection.receive()) self.assertEqual("Message receive timed out.", str(e.exception)) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) data = {"one": 1} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(data)) self.assertEqual(0, self.ws_connection.last_recv_time) response = self.async_run_with_timeout(self.ws_connection.receive()) self.assertIsInstance(response, WSResponse) self.assertEqual(data, response.data) self.assertNotEqual(0, self.ws_connection.last_recv_time) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_disconnects_and_raises_on_aiohttp_closed( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.close_code = 1111 self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.CLOSED) with self.assertRaises(ConnectionError) as e: self.async_run_with_timeout(self.ws_connection.receive()) self.assertEqual( "The WS connection was closed unexpectedly. Close code = 1111 msg data: ", str(e.exception)) self.assertFalse(self.ws_connection.connected) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_disconnects_and_raises_on_aiohttp_close( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.close_code = 1111 self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.CLOSE) with self.assertRaises(ConnectionError) as e: self.async_run_with_timeout(self.ws_connection.receive()) self.assertEqual( "The WS connection was closed unexpectedly. Close code = 1111 msg data: ", str(e.exception)) self.assertFalse(self.ws_connection.connected) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_ignores_aiohttp_close_msg_if_disconnect_called( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.CLOSED) prev_side_effect = ws_connect_mock.return_value.receive.side_effect async def disconnect_on_side_effect(*args, **kwargs): await self.ws_connection.disconnect() return await prev_side_effect(*args, **kwargs) ws_connect_mock.return_value.receive.side_effect = disconnect_on_side_effect response = self.async_run_with_timeout(self.ws_connection.receive()) self.assertFalse(self.ws_connection.connected) self.assertIsNone(response) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_ignores_ping(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.PING) data = {"one": 1} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(data)) response = self.async_run_with_timeout(self.ws_connection.receive()) self.assertEqual(data, response.data) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_sends_pong_on_ping(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.PING) receive_task = self.ev_loop.create_task(self.ws_connection.receive()) self.async_tasks.append(receive_task) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) ws_connect_mock.return_value.pong.assert_called() @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_ping_updates_last_recv_time(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.PING) receive_task = self.ev_loop.create_task(self.ws_connection.receive()) self.async_tasks.append(receive_task) self.assertEqual(0, self.ws_connection.last_recv_time) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertNotEqual(0, self.ws_connection.last_recv_time) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_ignores_pong(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.PONG) data = {"one": 1} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps(data)) response = self.async_run_with_timeout(self.ws_connection.receive()) self.assertEqual(data, response.data) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_receive_pong_updates_last_recv_time(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout(self.ws_connection.connect(self.ws_url)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=aiohttp.WSMsgType.PONG) receive_task = self.ev_loop.create_task(self.ws_connection.receive()) self.async_tasks.append(receive_task) self.assertEqual(0, self.ws_connection.last_recv_time) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertNotEqual(0, self.ws_connection.last_recv_time)
class BybitPerpetualUserStreamDataSourceTests(TestCase): # the level is required to receive logs from the data source loger level = 0 def setUp(self) -> None: super().setUp() self.api_key = 'testAPIKey' self.secret = 'testSecret' self.log_records = [] self.listening_task = None self.data_source = BybitPerpetualUserStreamDataSource( auth_assistant=BybitPerpetualAuth(api_key=self.api_key, secret_key=self.secret)) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() if self.data_source._session is not None: asyncio.get_event_loop().run_until_complete( self.data_source._session.close()) super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _authentication_response(self, authenticated: bool) -> str: request = { "op": "auth", "args": ['testAPIKey', 'testExpires', 'testSignature'] } message = { "success": authenticated, "ret_msg": "", "conn_id": "testConnectionID", "request": request } return message def _subscription_response(self, subscribed: bool, subscription: str) -> str: request = {"op": "subscribe", "args": [subscription]} message = { "success": subscribed, "ret_msg": "", "conn_id": "testConnectionID", "request": request } return message def _raise_exception(self, exception_class): raise exception_class @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_authenticates_and_subscribes_to_events( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) initial_last_recv_time = self.data_source.last_recv_time self.listening_task = asyncio.get_event_loop().create_task( self.data_source._listen_for_user_stream_on_url( "test_url", messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._subscription_response( True, CONSTANTS.WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME)) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._subscription_response( True, CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME)) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._subscription_response( True, CONSTANTS.WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME)) # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, json.dumps('dummyMessage')) asyncio.get_event_loop().run_until_complete(messages.get()) self.assertTrue( self._is_logged('INFO', "Authenticating to User Stream...")) self.assertTrue( self._is_logged('INFO', "Successfully authenticated to User Stream.")) self.assertTrue( self._is_logged( 'INFO', "Successful subscription to the topic ['position'] on test_url" )) self.assertTrue( self._is_logged( "INFO", "Successful subscription to the topic ['order'] on test_url")) self.assertTrue( self._is_logged( "INFO", "Successful subscription to the topic ['execution'] on test_url" )) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(4, len(sent_messages)) authentication_request = sent_messages[0] subscription_positions_request = sent_messages[1] subscription_orders_request = sent_messages[2] subscription_executions_request = sent_messages[3] self.assertEqual( CONSTANTS.WS_AUTHENTICATE_USER_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( authentication_request)) self.assertEqual( CONSTANTS.WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( subscription_positions_request)) self.assertEqual( CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( subscription_orders_request)) self.assertEqual( CONSTANTS.WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( subscription_executions_request)) subscription_positions_payload = BybitPerpetualWebSocketAdaptor.payload_from_message( subscription_positions_request) expected_payload = {"op": "subscribe", "args": ["position"]} self.assertEqual(expected_payload, subscription_positions_payload) subscription_orders_payload = BybitPerpetualWebSocketAdaptor.payload_from_message( subscription_orders_request) expected_payload = {"op": "subscribe", "args": ["order"]} self.assertEqual(expected_payload, subscription_orders_payload) subscription_executions_payload = BybitPerpetualWebSocketAdaptor.payload_from_message( subscription_executions_request) expected_payload = {"op": "subscribe", "args": ["execution"]} self.assertEqual(expected_payload, subscription_executions_payload) self.assertGreater(self.data_source.last_recv_time, initial_last_recv_time) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_fails_when_authentication_fails( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(False)) try: asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream " "(Could not authenticate websocket connection with Bybit Perpetual)" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. " "(Could not authenticate websocket connection with Bybit Perpetual)" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_initialization( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.WS_AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ "op"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_positions_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS. WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_orders_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_executions_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS. WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_initialization( self, ws_connect_mock): ws_connect_mock.side_effect = Exception with self.assertRaises(Exception): self.listening_task = asyncio.get_event_loop().create_task( self.data_source._create_websocket_connection("test_url")) asyncio.get_event_loop().run_until_complete(self.listening_task) self.assertTrue( self._is_logged( "NETWORK", "Unexpected error occurred during bybit_perpetual WebSocket Connection on test_url ()" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS.WS_AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ "op"] else self.mocking_assistant.add_websocket_json_message( sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream ()")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_positions_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS. WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred subscribing to bybit_perpetual private channels ()" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_orders_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred subscribing to bybit_perpetual private channels ()" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_executions_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS. WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred subscribing to bybit_perpetual private channels ()" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" ))
class NdaxAPIUserStreamDataSourceTests(TestCase): # the level is required to receive logs from the data source loger level = 0 def setUp(self) -> None: super().setUp() self.uid = '001' self.api_key = 'testAPIKey' self.secret = 'testSecret' self.account_id = 528 self.username = '******' self.oms_id = 1 self.log_records = [] self.listening_task = None throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) auth_assistant = NdaxAuth(uid=self.uid, api_key=self.api_key, secret_key=self.secret, account_name=self.username) self.data_source = NdaxAPIUserStreamDataSource(throttler, auth_assistant) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = asyncio.get_event_loop().run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _authentication_response(self, authenticated: bool) -> str: user = { "UserId": 492, "UserName": "******", "Email": "*****@*****.**", "EmailVerified": True, "AccountId": self.account_id, "OMSId": self.oms_id, "Use2FA": True } payload = { "Authenticated": authenticated, "SessionToken": "74e7c5b0-26b1-4ca5-b852-79b796b0e599", "User": user, "Locked": False, "Requires2FA": False, "EnforceEnable2FA": False, "TwoFAType": None, "TwoFAToken": None, "errormsg": None } message = { "m": 1, "i": 1, "n": CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME, "o": json.dumps(payload) } return json.dumps(message) def _raise_exception(self, exception_class): raise exception_class @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_authenticates_and_subscribes_to_events( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) initial_last_recv_time = self.data_source.last_recv_time self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, self._authentication_response(True)) # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps('dummyMessage')) first_received_message = self.async_run_with_timeout(messages.get()) self.assertEqual('dummyMessage', first_received_message) self.assertTrue( self._is_logged('INFO', "Authenticating to User Stream...")) self.assertTrue( self._is_logged('INFO', "Successfully authenticated to User Stream.")) self.assertTrue( self._is_logged('INFO', "Successfully subscribed to user events.")) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(2, len(sent_messages)) authentication_request = sent_messages[0] subscription_request = sent_messages[1] self.assertEqual( CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME, NdaxWebSocketAdaptor.endpoint_from_raw_message( json.dumps(authentication_request))) self.assertEqual( CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME, NdaxWebSocketAdaptor.endpoint_from_raw_message( json.dumps(subscription_request))) subscription_payload = NdaxWebSocketAdaptor.payload_from_raw_message( json.dumps(subscription_request)) expected_payload = {"AccountId": self.account_id, "OMSId": self.oms_id} self.assertEqual(expected_payload, subscription_payload) self.assertGreater(self.data_source.last_recv_time, initial_last_recv_time) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_fails_when_authentication_fails( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, self._authentication_response(False)) try: self.async_run_with_timeout(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream " "(Could not authenticate websocket connection with NDAX)")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. " "(Could not authenticate websocket connection with NDAX)")) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_initialization( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ 'n'] else self.mocking_assistant._sent_websocket_json_messages[ ws_connect_mock.return_value].append(sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_events_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS. SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message[ 'n'] else self.mocking_assistant._sent_websocket_json_messages[ ws_connect_mock.return_value].append(sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, self._authentication_response(True)) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_initialization( self, ws_connect_mock): ws_connect_mock.side_effect = Exception with self.assertRaises(Exception): self.listening_task = asyncio.get_event_loop().create_task( self.data_source._init_websocket_connection()) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "NETWORK", "Unexpected error occurred during ndax WebSocket Connection ()" )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ 'n'] else self.mocking_assistant._sent_websocket_json_messages[ ws_connect_mock.return_value].append(sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) self.async_run_with_timeout(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream ()")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. ()" )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_logs_exception_during_events_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = lambda sent_message: ( CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message[ 'n'] and self._raise_exception(Exception)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, self._authentication_response(True)) self.async_run_with_timeout(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred subscribing to ndax private channels ()")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. ()" ))
class AscendExAPIOrderBookDataSourceTests(TestCase): # logging.Level required to receive logs from the data source logger level = 0 def setUp(self) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.base_asset = "BTC" self.quote_asset = "USDT" self.trading_pair = f"{self.base_asset}-{self.quote_asset}" self.ex_trading_pair = f"{self.base_asset}/{self.quote_asset}" self.log_records = [] self.listening_task = None self.async_task: Optional[asyncio.Task] = None self.shared_client = None self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.data_source = AscendExAPIOrderBookDataSource( shared_client=self.shared_client, throttler=self.throttler, trading_pairs=[self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.data_source._trading_pair_symbol_map = {} self.mocking_assistant = NetworkMockingAssistant() self.resume_test_event = asyncio.Event() def tearDown(self) -> None: self.async_task and self.async_task.cancel() self.listening_task and self.listening_task.cancel() self.data_source._shared_client and self.data_source._shared_client.close( ) super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _raise_exception(self, exception_class): raise exception_class def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @aioresponses() def test_fetch_trading_pairs(self, api_mock): mock_response = { "code": 0, "data": [ { "symbol": self.ex_trading_pair, "open": "0.06777", "close": "0.06809", "high": "0.06899", "low": "0.06708", "volume": "19823722", "ask": ["0.0681", "43641"], "bid": ["0.0676", "443"], }, { "symbol": "BTC/USDT", "open": "0.06777", "close": "0.06809", "high": "0.06899", "low": "0.06708", "volume": "19823722", "ask": ["0.0681", "43641"], "bid": ["0.0676", "443"], }, { "symbol": "ETH/USDT", "open": "0.06777", "close": "0.06809", "high": "0.06899", "low": "0.06708", "volume": "19823722", "ask": ["0.0681", "43641"], "bid": ["0.0676", "443"], }, ], } url = f"{CONSTANTS.REST_URL}/{CONSTANTS.TICKER_PATH_URL}" api_mock.get(url, body=json.dumps(mock_response)) trading_pairs = self.async_run_with_timeout( self.data_source.fetch_trading_pairs( client=self.data_source._shared_client, throttler=self.throttler)) self.assertEqual(3, len(trading_pairs)) self.assertEqual("BTC-USDT", trading_pairs[1]) @aioresponses() def test_get_last_traded_prices_requests_rest_api_price_when_subscription_price_not_available( self, api_mock): mock_response = { "code": 0, "data": { "m": "trades", "symbol": "BTC/USDT", "data": [ { "seqnum": 144115191800016553, "p": "0.06762", "q": "400", "ts": 1573165890854, "bm": False }, { "seqnum": 144115191800070421, "p": "0.06797", "q": "341", "ts": 1573166037845, "bm": True }, ], }, } self.data_source._trading_pairs = ["BTC-USDT"] url = re.escape( f"{CONSTANTS.REST_URL}/{CONSTANTS.TRADES_PATH_URL}?symbol=") regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) results = self.ev_loop.run_until_complete( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], client=self.data_source._shared_client, throttler=self.throttler)) self.assertEqual(results[self.trading_pair], float(mock_response["data"]["data"][1]["p"])) @aioresponses() def test_get_order_book_http_error_raises_exception(self, api_mock): mock_response = "ERROR WITH REQUEST" url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}" regex_url = re.compile(f"^{url}") api_mock.get(regex_url, status=400, body=mock_response) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_order_book_data( trading_pair=self.trading_pair, throttler=self.throttler)) @aioresponses() def test_get_order_book_resp_code_erro_raises_exception(self, api_mock): mock_response = { "code": 100001, "reason": "INVALID_HTTP_INPUT", "message": "Http request is invalid" } url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}" regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_order_book_data( trading_pair=self.trading_pair, throttler=self.throttler)) @aioresponses() def test_get_order_book_data_successful(self, api_mock): mock_response = { "code": 0, "data": { "m": "depth-snapshot", "symbol": self.ex_trading_pair, "data": { "seqnum": 5068757, "ts": 1573165838976, "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]], "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]], }, }, } url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}" regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_order_book_data( trading_pair=self.trading_pair, throttler=self.throttler)) self.assertTrue(result.get("symbol") == self.ex_trading_pair) @aioresponses() def test_get_new_order_book(self, api_mock): mock_response = { "code": 0, "data": { "m": "depth-snapshot", "symbol": "BTC/USDT", "data": { "seqnum": 5068757, "ts": 1573165838976, "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]], "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]], }, }, } self.data_source._trading_pairs = ["BTC-USDT"] # path_url = ascend_ex_utils.rest_api_path_for_endpoint(CONSTANTS.ORDER_BOOK_ENDPOINT, self.trading_pair) url = re.escape( f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}?symbol=") regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) self.listening_task = self.ev_loop.create_task( self.data_source.get_new_order_book(self.trading_pair)) order_book = self.ev_loop.run_until_complete(self.listening_task) bids = list(order_book.bid_entries()) asks = list(order_book.ask_entries()) self.assertEqual(2, len(bids)) self.assertEqual(0.06703, round(bids[0].price, 5)) self.assertEqual(13500, round(bids[0].amount, 1)) self.assertEqual(1573165838976, bids[0].update_id) self.assertEqual(2, len(asks)) self.assertEqual(0.06848, round(asks[0].price, 5)) self.assertEqual(4084.2, round(asks[0].amount, 1)) self.assertEqual(1573165838976, asks[0].update_id) @patch("aiohttp.client.ClientSession.ws_connect") def test_subscribe_to_order_book_streams_raises_exception( self, ws_connect_mock): ws_connect_mock.side_effect = Exception("TEST ERROR") with self.assertRaisesRegex(Exception, "TEST ERROR"): self.async_run_with_timeout( self.data_source._subscribe_to_order_book_streams()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) @patch("aiohttp.client.ClientSession.ws_connect") def test_subscribe_to_order_book_streams_raises_cancel_exception( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source._subscribe_to_order_book_streams()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_subscribe_to_order_book_streams_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout( self.data_source._subscribe_to_order_book_streams()) self.assertTrue( self._is_logged( "INFO", f"Subscribed to ['{self.trading_pair}'] orderbook trading and delta streams..." )) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) stream_topics = [payload["ch"] for payload in sent_messages] self.assertEqual(2, len(stream_topics)) self.assertTrue( f"{self.data_source.DIFF_TOPIC_ID}:{self.ex_trading_pair}" in stream_topics) self.assertTrue( f"{self.data_source.TRADE_TOPIC_ID}:{self.ex_trading_pair}" in stream_topics) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_exception_raised_cancelled_when_connecting( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_exception_raised_cancelled_when_subscribing( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_exception_raised_cancelled_when_listening( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source.AscendExAPIOrderBookDataSource._sleep" ) def test_listen_for_subscription_logs_exception(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = lambda: self._create_exception_and_unlock_test_with_event( Exception("TEST ERROR")) self.async_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred iterating through websocket messages." )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_enqueues_diff_and_trade_messages( self, ws_connect_mock): diffs_queue = self.data_source._message_queue[ self.data_source.DIFF_TOPIC_ID] trade_queue = self.data_source._message_queue[ self.data_source.TRADE_TOPIC_ID] ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add diff event message be processed diff_response = { "m": "depth", "symbol": self.ex_trading_pair, "data": { "ts": 1573069021376, "seqnum": 2097965, "asks": [["0.06844", "10760"]], "bids": [["0.06777", "562.4"], ["0.05", "221760.6"]], }, } # Add trade event message be processed trade_response = { "m": "trades", "symbol": "BTC/USDT", "data": [ { "seqnum": 144115191800016553, "p": "0.06762", "q": "400", "ts": 1573165890854, "bm": False }, { "seqnum": 144115191800070421, "p": "0.06797", "q": "341", "ts": 1573166037845, "bm": True }, ], } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(trade_response)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(1, diffs_queue.qsize()) self.assertEqual(1, trade_queue.qsize()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_handle_ping_message( self, ws_connect_mock): # In AscendEx Ping message is sent as a aiohttp.WSMsgType.TEXT message mock_response = {"m": "ping", "hp": 3} ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(mock_response), message_type=aiohttp.WSMsgType.TEXT, ) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_json = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertTrue(any(["pong" in str(payload) for payload in sent_json])) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diff_raises_cancel_exceptions( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop, output=queue)) with self.assertRaises(asyncio.CancelledError): self.listening_task.cancel() self.ev_loop.run_until_complete(self.listening_task) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source.AscendExAPIOrderBookDataSource._sleep" ) def test_listen_for_order_book_diff_logs_exception_parsing_message( self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add incomplete diff event message be processed diff_response = {"m": "depth", "symbol": "incomplete response"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop, output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diff_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add diff event message be processed diff_response = { "m": "depth", "symbol": self.ex_trading_pair, "data": { "ts": 1573069021376, "seqnum": 2097965, "asks": [["0.06844", "10760"]], "bids": [["0.06777", "562.4"], ["0.05", "221760.6"]], }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) diffs_queue = self.data_source._message_queue[ self.data_source.DIFF_TOPIC_ID] self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop, output=output_queue)) order_book_message = self.async_run_with_timeout(output_queue.get()) self.assertTrue(diffs_queue.empty()) self.assertEqual(1573069021376, order_book_message.update_id) self.assertEqual(1573069021376, order_book_message.timestamp) self.assertEqual(0.06777, order_book_message.bids[0].price) self.assertEqual(0.05, order_book_message.bids[1].price) self.assertEqual(0.06844, order_book_message.asks[0].price) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_raises_cancel_exceptions(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=queue)) with self.assertRaises(asyncio.CancelledError): self.listening_task.cancel() self.ev_loop.run_until_complete(self.listening_task) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source.AscendExAPIOrderBookDataSource._sleep" ) def test_listen_for_trades_logs_exception_parsing_message( self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add incomplete diff event message be processed diff_response = {"m": "trades", "symbol": "incomplete response"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add trade event message be processed trade_response = { "m": "trades", "symbol": "BTC/USDT", "data": [ { "seqnum": 144115191800016553, "p": "0.06762", "q": "400", "ts": 1573165890854, "bm": False }, { "seqnum": 144115191800070421, "p": "0.06797", "q": "341", "ts": 1573166037845, "bm": True }, ], } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(trade_response)) trades_queue = self.data_source._message_queue[ self.data_source.DIFF_TOPIC_ID] self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=output_queue)) first_trade_message = self.async_run_with_timeout(output_queue.get()) second_trade_message = self.async_run_with_timeout(output_queue.get()) self.assertTrue(trades_queue.empty()) self.assertEqual(1573165890854, first_trade_message.timestamp) self.assertEqual(1573166037845, second_trade_message.timestamp) @aioresponses() def test_listen_for_order_book_snapshot_event(self, api_mock): mock_response = { "code": 0, "data": { "m": "depth-snapshot", "symbol": self.ex_trading_pair, "data": { "seqnum": 5068757, "ts": 1573165838976, "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]], "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]], }, }, } self.data_source._trading_pairs = ["BTC-USDT"] # Add trade event message be processed url = re.escape( f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}?symbol=") regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) order_book_messages = asyncio.Queue() task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( ev_loop=self.ev_loop, output=order_book_messages)) order_book_message = self.ev_loop.run_until_complete( order_book_messages.get()) try: task.cancel() self.ev_loop.run_until_complete(task) except asyncio.CancelledError: # The exception will happen when cancelling the task pass self.assertTrue(order_book_messages.empty()) self.assertEqual(1573165838976, order_book_message.update_id) self.assertEqual(1573165838976, order_book_message.timestamp) self.assertEqual(0.06703, order_book_message.bids[0].price) self.assertEqual(0.06848, order_book_message.asks[0].price)
class GateIoWebsocketTest(unittest.TestCase): def setUp(self) -> None: self.ev_loop = asyncio.get_event_loop() self.ws = GateIoWebsocket() self.mocking_assistant = NetworkMockingAssistant() def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_subscribe(self, mock_ws): subscription_channel = "someChannel" mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.ws.connect()) ret = self.async_run_with_timeout(self.ws.subscribe(channel=subscription_channel)) np.testing.assert_allclose([time.time()], [ret], rtol=1) calls = self.mocking_assistant.json_messages_sent_through_websocket(mock_ws.return_value) self.assertEqual(1, len(calls)) subscription_call = calls[0] self.assertEqual("subscribe", subscription_call["event"]) self.assertEqual(ret, subscription_call["time"]) self.assertEqual(subscription_channel, subscription_call["channel"]) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_unsubscribe(self, mock_ws): subscription_channel = "someChannel" mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.ws.connect()) ret = self.async_run_with_timeout(self.ws.unsubscribe(channel=subscription_channel)) np.testing.assert_allclose([time.time()], [ret], rtol=1) calls = self.mocking_assistant.json_messages_sent_through_websocket(mock_ws.return_value) self.assertEqual(1, len(calls)) subscription_call = calls[0] self.assertEqual("unsubscribe", subscription_call["event"]) self.assertEqual(ret, subscription_call["time"]) self.assertEqual(subscription_channel, subscription_call["channel"]) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_on_message(self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.ws.connect()) async_iter = self.ws.on_message() mock_event = "somEvent" self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps({"event": mock_event}) ) ret = self.async_run_with_timeout(async_iter.__anext__(), timeout=0.1) self.assertEqual(mock_event, ret["event"]) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_ping_sent_pong_ignored(self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.closed = False self.async_run_with_timeout(self.ws.connect()) async_iter = self.ws.on_message() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, message="", message_type=aiohttp.WSMsgType.PONG # should be ignored ) mock_event = "somEvent" self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps({"event": mock_event}) ) ret = self.async_run_with_timeout(async_iter.__anext__(), timeout=1) mock_ws.return_value.ping.assert_called() self.assertEqual(mock_event, ret["event"]) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_last_recv_time_set_on_pong(self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.closed = False self.async_run_with_timeout(self.ws.connect()) async_iter = self.ws.on_message() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, message="", message_type=aiohttp.WSMsgType.PONG # should be ignored ) anext_task = self.ev_loop.create_task(async_iter.__anext__()) try: self.mocking_assistant.run_until_all_aiohttp_messages_delivered(mock_ws.return_value) np.testing.assert_allclose([self.ws.last_recv_time], [time.time()], rtol=1) except Exception: raise finally: anext_task.cancel() @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_backup_ping_pong(self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.closed = False default_receive = mock_ws.return_value.receive.side_effect self.async_run_with_timeout(self.ws.connect()) async_iter = self.ws.on_message() async def switch_back_and_raise_timeout(*args, **kwargs): mock_ws.return_value.receive.side_effect = default_receive raise asyncio.TimeoutError mock_ws.return_value.receive.side_effect = switch_back_and_raise_timeout self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps({"channel": "spot.pong"}) # should be ignored ) mock_event = "somEvent" self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps({"event": mock_event}) ) ret = self.async_run_with_timeout(async_iter.__anext__(), timeout=1) self.assertEqual(mock_event, ret["event"])
class CryptoComWebSocketUnitTests(unittest.TestCase): # the level is required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.trading_pairs = ["COINALPHA-HBOT"] cls.api_key = "someKey" cls.secret_key = "someSecretKey" cls.auth = CryptoComAuth(api_key=cls.api_key, secret_key=cls.secret_key) def setUp(self) -> None: super().setUp() self.log_records = [] self.websocket = CryptoComWebsocket(auth=self.auth) self.websocket.logger().setLevel(1) self.websocket.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() self.async_task: Optional[asyncio.Task] = None self.resume_test_event = asyncio.Event() def tearDown(self) -> None: self.ev_loop.run_until_complete(self.websocket.disconnect()) self.async_task and self.async_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any(record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def resume_test_callback(self): self.resume_test_event.set() async def _iter_message(self): async for _ in self.websocket.iter_messages(): self.resume_test_callback() self.async_task.cancel() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep") def test_connect_raises_exception(self, _, ws_connect_mock): ws_connect_mock.side_effect = Exception("TEST ERROR") self.websocket = CryptoComWebsocket() with self.assertRaisesRegex(Exception, "TEST ERROR"): self.async_run_with_timeout(self.websocket.connect()) self.assertTrue(self._is_logged("ERROR", "Websocket error: 'TEST ERROR'")) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep") def test_connect_authenticate_is_called(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.websocket.connect()) sent_payloads = self.mocking_assistant.json_messages_sent_through_websocket(ws_connect_mock.return_value) self.assertEqual(1, len(sent_payloads)) self.assertEqual(CryptoComWebsocket.AUTH_REQUEST, sent_payloads[0]["method"]) def test_disconnect(self): ws = AsyncMock() self.websocket._websocket = ws self.async_run_with_timeout(self.websocket.disconnect()) self.assertEqual(1, ws.close.await_count) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep") def test_iter_messages_handle_ping(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.websocket.connect()) mock_ping = {"id": 1587523073344, "method": "public/heartbeat", "code": 0} expected_pong_payload = {'id': 1587523073344, 'method': 'public/respond-heartbeat'} self.mocking_assistant.add_websocket_aiohttp_message(ws_connect_mock.return_value, json.dumps(mock_ping)) self.async_task = self.ev_loop.create_task(self._iter_message()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) sent_payloads = self.mocking_assistant.json_messages_sent_through_websocket(ws_connect_mock.return_value) self.assertEqual(2, len(sent_payloads)) self.assertEqual(expected_pong_payload, sent_payloads[-1]) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep") def test_subscribe_to_order_book_streams_raises_cancelled_exception(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.websocket.connect()) ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout(self.websocket.subscribe_to_order_book_streams(self.trading_pairs)) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep") def test_subscribe_to_order_book_streams_logs_exception(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.websocket.connect()) ws_connect_mock.return_value.send_json.side_effect = Exception("TEST ERROR") with self.assertRaisesRegex(Exception, "TEST ERROR"): self.async_run_with_timeout(self.websocket.subscribe_to_order_book_streams(self.trading_pairs)) self.assertTrue(self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep") def test_subscribe_to_user_streams_raises_cancelled_exception(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.websocket.connect()) ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout(self.websocket.subscribe_to_user_streams()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep") def test_subscribe_to_user_streams_logs_exception(self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.async_run_with_timeout(self.websocket.connect()) ws_connect_mock.return_value.send_json.side_effect = Exception("TEST ERROR") with self.assertRaisesRegex(Exception, "TEST ERROR"): self.async_run_with_timeout(self.websocket.subscribe_to_user_streams()) self.assertTrue(self._is_logged( "ERROR", "Unexpected error occurred subscribing to user streams..." ))
class TestKucoinAPIUserStreamDataSource(unittest.TestCase): # the level is required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_key = "someKey" cls.api_passphrase = "somePassPhrase" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task: Optional[asyncio.Task] = None self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.mock_time_provider = MagicMock() self.mock_time_provider.time.return_value = 1000 self.auth = KucoinAuth(self.api_key, self.api_passphrase, self.api_secret_key, time_provider=self.mock_time_provider) self.time_synchronizer = TimeSynchronizer() self.time_synchronizer.add_time_offset_ms_sample(0) self.api_factory = web_utils.build_api_factory( throttler=self.throttler, time_synchronizer=self.time_synchronizer, auth=self.auth) self.data_source = KucoinAPIUserStreamDataSource( throttler=self.throttler, api_factory=self.api_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @staticmethod def get_listen_key_mock(): listen_key = { "code": "200000", "data": { "token": "someToken", "instanceServers": [{ "endpoint": "wss://someEndpoint", "encrypt": True, "protocol": "websocket", "pingInterval": 18000, "pingTimeout": 10000, }] } } return listen_key def test_last_recv_time(self): # Initial last_recv_time self.assertEqual(0, self.data_source.last_recv_time) ws_assistant = self.async_run_with_timeout( self.data_source._get_ws_assistant()) ws_assistant._connection._last_recv_time = 1000 self.assertEqual(1000, self.data_source.last_recv_time) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id" ) def test_listen_for_user_stream_subscribes_to_orders_and_balances_events( self, mock_api, id_mock, ws_connect_mock): id_mock.side_effect = [1, 2] url = web_utils.rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [{ "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 50000, "pingTimeout": 10000 }], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = {"type": "ack", "id": 1} result_subscribe_diffs = {"type": "ack", "id": 2} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_subscription_messages)) expected_orders_subscription = { "id": 1, "type": "subscribe", "topic": "/spotMarket/tradeOrders", "privateChannel": True, "response": False } self.assertEqual(expected_orders_subscription, sent_subscription_messages[0]) expected_balances_subscription = { "id": 2, "type": "subscribe", "topic": "/account/balance", "privateChannel": True, "response": False } self.assertEqual(expected_balances_subscription, sent_subscription_messages[1]) self.assertTrue( self._is_logged( "INFO", "Subscribed to private order changes and balance updates channels..." )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_get_listen_key_successful_with_user_update_event( self, mock_api, mock_ws): url = web_utils.rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_api.post(regex_url, body=json.dumps(mock_response)) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() order_event = { "type": "message", "topic": "/spotMarket/tradeOrders", "subject": "orderChange", "channelType": "private", "data": { "symbol": "KCS-USDT", "orderType": "limit", "side": "buy", "orderId": "5efab07953bdea00089965d2", "type": "open", "orderTime": 1593487481683297666, "size": "0.1", "filledSize": "0", "price": "0.937", "clientOid": "1593487481000906", "remainSize": "0.1", "status": "open", "ts": 1593487481683297666 } } self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps(order_event)) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) msg = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(order_event, msg) mock_ws.return_value.ping.assert_called() @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_does_not_queue_pong_payload( self, mock_api, mock_ws): url = web_utils.rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_pong = {"id": "1545910590801", "type": "pong"} mock_api.post(regex_url, body=json.dumps(mock_response)) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps(mock_pong)) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize()) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep" ) def test_listen_for_user_stream_connection_failed(self, mock_api, sleep_mock, mock_ws): url = web_utils.rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_api.post(regex_url, body=json.dumps(mock_response)) mock_ws.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = asyncio.CancelledError # to finish the task execution msg_queue = asyncio.Queue() try: self.async_run_with_timeout( self.data_source.listen_for_user_stream(msg_queue)) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to user streams. Retrying in 5 seconds..." )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep" ) def test_listen_for_user_stream_iter_message_throws_exception( self, mock_api, sleep_mock, mock_ws): url = web_utils.rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_api.post(regex_url, body=json.dumps(mock_response)) msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.receive.side_effect = Exception("TEST ERROR") sleep_mock.side_effect = asyncio.CancelledError # to finish the task execution try: self.async_run_with_timeout( self.data_source.listen_for_user_stream(msg_queue)) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to user streams. Retrying in 5 seconds..." )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id" ) @patch( "hummingbot.connector.exchange.kucoin.kucoin_api_user_stream_data_source.KucoinAPIUserStreamDataSource" "._time") def test_listen_for_user_stream_sends_ping_message_before_ping_interval_finishes( self, mock_api, time_mock, id_mock, ws_connect_mock): id_mock.side_effect = [1, 2, 3, 4] time_mock.side_effect = [ 1000, 1100, 1101, 1102 ] # Simulate first ping interval is already due url = web_utils.rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [{ "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 20000, "pingTimeout": 10000 }], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = {"type": "ack", "id": 1} result_subscribe_diffs = {"type": "ack", "id": 2} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) expected_ping_message = { "id": 3, "type": "ping", } self.assertEqual(expected_ping_message, sent_messages[-1])
class ProbitAPIUserStreamDataSourceTest(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: cls.base_asset = "BTC" cls.quote_asset = "USDT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.api_key = "someKey" self.api_secret = "someSecret" self.auth = ProbitAuth(self.api_key, self.api_secret) self.data_source = ProbitAPIUserStreamDataSource( self.auth, trading_pairs=[self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.log_records = [] self.mocking_assistant = NetworkMockingAssistant() self.async_task: Optional[asyncio.Task] = None def tearDown(self) -> None: self.async_task and self.async_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def check_is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.probit.probit_auth.ProbitAuth.get_ws_auth_payload", new_callable=AsyncMock, ) def test_listen_for_user_stream(self, get_ws_auth_payload_mock, ws_connect_mock): auth_msg = {"type": "authorization", "token": "someToken"} get_ws_auth_payload_mock.return_value = auth_msg ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, message={"result": "ok"} # authentication ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps({"my_msg": "test"}) # first message ) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, output_queue)) self.mocking_assistant.run_until_all_json_messages_delivered( ws_connect_mock.return_value) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) sent_text_msgs = self.mocking_assistant.text_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(auth_msg, json.loads(sent_text_msgs[0])) sent_json_msgs = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) for sent_json_msg in sent_json_msgs: self.assertEqual("subscribe", sent_json_msg["type"]) self.assertIn(sent_json_msg["channel"], CONSTANTS.WS_PRIVATE_CHANNELS) CONSTANTS.WS_PRIVATE_CHANNELS.remove(sent_json_msg["channel"]) self.assertEqual(0, len(CONSTANTS.WS_PRIVATE_CHANNELS)) self.assertNotEqual(0, self.data_source.last_recv_time) @patch("aiohttp.client.ClientSession.ws_connect") @patch( "hummingbot.connector.exchange.probit.probit_api_user_stream_data_source.ProbitAPIUserStreamDataSource._sleep", new_callable=AsyncMock, ) def test_listen_for_user_stream_attempts_again_on_exception( self, sleep_mock, ws_connect_mock): called_event = asyncio.Event() async def _sleep(delay): called_event.set() await asyncio.sleep(delay) sleep_mock.side_effect = _sleep ws_connect_mock.side_effect = Exception self.async_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, asyncio.Queue())) self.async_run_with_timeout(called_event.wait()) self.check_is_logged( log_level="ERROR", message= "Unexpected error with Probit WebSocket connection. Retrying after 30 seconds...", ) @patch("aiohttp.client.ClientSession.ws_connect") def test_listen_for_user_stream_stops_on_asyncio_cancelled_error( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_user_stream( self.ev_loop, asyncio.Queue())) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.probit.probit_auth.ProbitAuth.get_ws_auth_payload", new_callable=AsyncMock, ) def test_listen_for_user_stream_registers_ping_msg( self, get_ws_auth_payload_mock, ws_connect_mock): auth_msg = {"type": "authorization", "token": "someToken"} get_ws_auth_payload_mock.return_value = auth_msg ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, message={"result": "ok"} # authentication ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=WSMsgType.PING) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue(output_queue.empty()) ws_connect_mock.return_value.pong.assert_called()
class ProbitAPIOrderBookDataSourceTest(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: cls.base_asset = "BTC" cls.quote_asset = "USDT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.domain = "com" def setUp(self) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.api_key = "someKey" self.api_secret = "someSecret" self.data_source = ProbitAPIOrderBookDataSource( trading_pairs=[self.trading_pair], domain=self.domain) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.log_records = [] self.mocking_assistant = NetworkMockingAssistant() self.async_tasks: List[asyncio.Task] = [] def tearDown(self) -> None: for task in self.async_tasks: task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def check_is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def get_ticker_resp_mock( self, last_price: float) -> Dict[str, List[Dict[str, str]]]: ticker_resp_mock = { "data": [{ "last": str(last_price), "low": "3235", "high": "3273.8", "change": "-23.4", "base_volume": "0.16690818", "quote_volume": "543.142095541", "market_id": self.trading_pair, "time": "2018-12-17T06:49:08.000Z" }] } return ticker_resp_mock @staticmethod def get_market_resp_mock( trading_pairs: List[str]) -> Dict[str, List[Dict[str, str]]]: market_resp_mock = { "data": [{ "id": trading_pair, "base_currency_id": trading_pair.split("-")[0], "quote_currency_id": trading_pair.split("-")[1], "closed": False, } for trading_pair in trading_pairs] } return market_resp_mock def get_order_book_resp_mock( self, ask_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None, bid_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None, ) -> Dict[str, List[Dict[str, str]]]: data = self.get_order_book_resp_mock_data(ask_price_quantity_tuples, bid_price_quantity_tuples) order_book_resp_mock = {"data": data} return order_book_resp_mock def get_marketdata_recent_trades_msg_mock( self, price_quantity_tuples: Optional[List[Tuple[float, float]]] = None, snapshot: bool = False, ) -> Dict: msg_mock = self.get_base_marketdata_mock_msg(snapshot) data = self.get_recent_trades_resp_mock_data(price_quantity_tuples) msg_mock["recent_trades"] = data return msg_mock def get_marketdata_order_books_msg_mock( self, ask_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None, bid_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None, snapshot: bool = False, ): msg_mock = self.get_base_marketdata_mock_msg(snapshot) data = self.get_order_book_resp_mock_data(ask_price_quantity_tuples, bid_price_quantity_tuples) msg_mock["order_books"] = data return msg_mock def get_base_marketdata_mock_msg(self, snapshot: bool = False) -> Dict: msg_mock = { "channel": "marketdata", "market_id": self.trading_pair, "status": "ok", "lag": 0, "ticker": { "time": "2018-08-17T03:00:43.000Z", "last": "0.00004221", "low": "0.00003953", "high": "0.00004233", "change": "0.00000195", "base_volume": "119304953.57728445", "quote_volume": "4914.391934022046355" }, "reset": snapshot, } return msg_mock @staticmethod def get_recent_trades_resp_mock_data( price_quantity_tuples: Optional[List[Tuple[float, float]]] = None ) -> List[Dict[str, str]]: price_quantity_tuples = price_quantity_tuples or [] trades_data = [{ "price": str(price), "quantity": str(quantity), "time": "2018-08-17T02:56:17.249Z", "side": "buy", "tick_direction": "zeroup", } for price, quantity in price_quantity_tuples] return trades_data @staticmethod def get_order_book_resp_mock_data( ask_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None, bid_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None, ) -> List[Dict[str, str]]: ask_price_quantity_tuples = ask_price_quantity_tuples or [] bid_price_quantity_tuples = bid_price_quantity_tuples or [] ask_data = [{ "side": "sell", "price": str(price), "quantity": str(quantity) } for price, quantity in ask_price_quantity_tuples] bid_data = [{ "side": "buy", "price": str(price), "quantity": str(quantity) } for price, quantity in bid_price_quantity_tuples] data = ask_data + bid_data return data @aioresponses() def test_get_last_traded_prices(self, mocked_api): last_price = 3252.4 url = f"{CONSTANTS.TICKER_URL.format(self.domain)}" resp = self.get_ticker_resp_mock(last_price) mocked_api.get(url, body=json.dumps(resp)) res = self.async_run_with_timeout( self.data_source.get_last_traded_prices([self.trading_pair], self.domain)) self.assertIn(self.trading_pair, res) self.assertEqual(last_price, res[self.trading_pair]) @aioresponses() def test_fetch_trading_pairs(self, mocked_api): other_pair = "BTC-USDT" url = f"{CONSTANTS.MARKETS_URL.format(self.domain)}" resp = self.get_market_resp_mock( trading_pairs=[self.trading_pair, other_pair]) mocked_api.get(url, body=json.dumps(resp)) res = self.async_run_with_timeout( self.data_source.fetch_trading_pairs(self.domain)) self.assertEqual(2, len(res)) self.assertIn(self.trading_pair, res) self.assertIn(other_pair, res) @aioresponses() def test_get_order_book_data(self, mocked_api): url = f"{CONSTANTS.ORDER_BOOK_URL.format(self.domain)}" regex_url = re.compile(f"^{url}") resp = self.get_order_book_resp_mock( ask_price_quantity_tuples=[(1, 2), (3, 4)]) mocked_api.get(regex_url, body=json.dumps(resp)) res = self.async_run_with_timeout( self.data_source.get_order_book_data(self.trading_pair, self.domain)) self.assertEqual(res, resp) res_data = res["data"] for d in resp["data"]: self.assertIn(d, res_data) @aioresponses() def test_get_new_order_book(self, mocked_api): first_ask_price = 1 url = f"{CONSTANTS.ORDER_BOOK_URL.format(self.domain)}" regex_url = re.compile(f"^{url}") resp = self.get_order_book_resp_mock( ask_price_quantity_tuples=[(first_ask_price, 2), (3, 4)]) mocked_api.get(regex_url, body=json.dumps(resp)) res = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertIsInstance(res, OrderBook) asks = list(res.ask_entries()) bids = list(res.bid_entries()) self.assertEqual(0, len(bids)) self.assertEqual(2, len(asks)) self.assertEqual(first_ask_price, asks[0].price) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.probit.probit_api_order_book_data_source.ProbitAPIOrderBookDataSource._sleep", new_callable=AsyncMock, ) def test_listen_for_subscriptions_logs_error_on_closed( self, sleep_mock, ws_connect_mock): called_event = asyncio.Event() continue_event = asyncio.Event() async def close_(): called_event.set() await continue_event.wait() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.close.side_effect = close_ self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=WSMsgType.CLOSED) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.async_run_with_timeout(called_event.wait()) log_target = ( # from _iter_messages "Unexpected error occurred iterating through websocket messages.") self.assertTrue( self.check_is_logged(log_level="ERROR", message=log_target)) called_event.clear() continue_event.set() self.async_run_with_timeout(called_event.wait()) log_target = ( # from listen_for_subscriptions "Unexpected error occurred when listening to order book streams. " "Retrying in 5 seconds...") self.assertTrue( self.check_is_logged(log_level="ERROR", message=log_target)) sleep_mock.assert_called_with(5.0) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_subscribes_to_order_book_streams( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="") self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_msgs = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertGreaterEqual(len(sent_msgs), 1) msg_filters = sent_msgs[0]["filter"] self.assertIn(self.data_source.TRADE_FILTER_ID, msg_filters) self.assertIn(self.data_source.DIFF_FILTER_ID, msg_filters) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades(self, ws_connect_mock): trade_price = 1 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) message = self.get_marketdata_recent_trades_msg_mock( price_quantity_tuples=[(trade_price, 2)]) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(message)) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) outputs = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, outputs))) res = self.async_run_with_timeout(outputs.get()) self.assertIsInstance(res, OrderBookMessage) self.assertEqual(str(trade_price), res.content["price"]) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_ignores_snapshot_msg(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) message = self.get_marketdata_recent_trades_msg_mock( price_quantity_tuples=[(1, 2)], snapshot=True # should be ignored ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(message)) trade_price = 2 message = self.get_marketdata_recent_trades_msg_mock( price_quantity_tuples=[(trade_price, 2)], snapshot=False) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(message)) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) outputs = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, outputs))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) res = self.async_run_with_timeout(outputs.get()) self.assertEqual(str(trade_price), res.content["price"]) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_generates_snapshot_msg( self, ws_connect_mock): first_ask_price = 1 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) message = self.get_marketdata_order_books_msg_mock( ask_price_quantity_tuples=[(first_ask_price, 2), (3, 4)], snapshot=True) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(message)) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) outputs = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, outputs))) res = self.async_run_with_timeout(outputs.get()) self.assertIsInstance(res, OrderBookMessage) self.assertEqual(2, len(res.asks)) self.assertEqual(0, len(res.bids)) self.assertEqual(first_ask_price, res.asks[0].price) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_generates_diff_msg( self, ws_connect_mock): ask_price = 1 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) message = self.get_marketdata_order_books_msg_mock( ask_price_quantity_tuples=[(ask_price, 2)], snapshot=False) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(message)) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) outputs = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, outputs))) res = self.async_run_with_timeout(outputs.get()) self.assertIsInstance(res, OrderBookMessage) self.assertEqual(1, len(res.asks)) self.assertEqual(0, len(res.bids)) self.assertEqual(ask_price, res.asks[0].price)
class HuobiAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}{cls.quote_asset}".lower() def setUp(self) -> None: super().setUp() self.log_records = [] self.async_tasks: List[asyncio.Task] = [] self.data_source = HuobiAPIOrderBookDataSource( trading_pairs=[self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() self.resume_test_event = asyncio.Event() def tearDown(self) -> None: for task in self.async_tasks: task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _compress(self, message: Dict[str, Any]) -> bytes: return gzip.compress(json.dumps(message).encode()) @aioresponses() def test_last_traded_prices(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.TICKER_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { "data": [ { "symbol": self.ex_trading_pair, "open": 1.1, "high": 2.0, "low": 0.8, "close": 1.5, "amount": 100, "vol": 100, "count": 100, "bid": 1.3, "bidSize": 10, "ask": 1.4, "askSize": 10, }, ], "status": "ok", "ts": 1637229769083, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair])) self.assertEqual(1, len(result)) self.assertIn(self.trading_pair, result) self.assertEqual(1.5, result[self.trading_pair]) @aioresponses() def test_fetch_trading_pairs_failed(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.API_VERSION + CONSTANTS.SYMBOLS_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=ujson.dumps({})) result = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) @aioresponses() def test_fetch_trading_pairs_successful(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.API_VERSION + CONSTANTS.SYMBOLS_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "status": "ok", "data": [{ "base-currency": self.base_asset.lower(), "quote-currency": self.quote_asset.lower(), "price-precision": 4, "amount-precision": 2, "symbol-partition": "innovation", "symbol": self.ex_trading_pair, "state": "online", "value-precision": 8, "min-order-amt": 1, "max-order-amt": 10000000, "min-order-value": 0.1, "limit-order-min-order-amt": 1, "limit-order-max-order-amt": 10000000, "limit-order-max-buy-amt": 10000000, "limit-order-max-sell-amt": 10000000, "sell-market-min-order-amt": 1, "sell-market-max-order-amt": 1000000, "buy-market-max-order-value": 17000, "api-trading": "enabled", "tags": "abnormalmarket", }], } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(1, len(result)) @aioresponses() def test_get_snapshot_raises_error(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=ujson.dumps({})) expected_error_msg = f"Error fetching Huobi market snapshot for {self.trading_pair}. HTTP status is 400" with self.assertRaisesRegex(IOError, expected_error_msg): self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) @aioresponses() def test_get_snapshot_successful(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "status": "ok", "ts": 1637255180894, "tick": { "bids": [ [57069.57, 0.05], ], "asks": [ [57057.73, 0.007019], ], "version": 141982962388, "ts": 1637255180700, }, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) self.assertEqual(mock_response["ch"], result["ch"]) self.assertEqual(mock_response["status"], result["status"]) self.assertEqual(1, len(result["tick"]["bids"])) self.assertEqual(1, len(result["tick"]["asks"])) @aioresponses() def test_get_new_order_book(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "status": "ok", "ts": 1637255180894, "tick": { "bids": [ [57069.57, 0.05], ], "asks": [ [57057.73, 0.007019], ], "version": 141982962388, "ts": 1637255180700, }, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertIsInstance(result, OrderBook) self.assertEqual(1637255180700, result.snapshot_uid) self.assertEqual(1, len(list(result.bid_entries()))) self.assertEqual(1, len(list(result.ask_entries()))) self.assertEqual(57069.57, list(result.bid_entries())[0].price) self.assertEqual(0.05, list(result.bid_entries())[0].amount) self.assertEqual(57057.73, list(result.ask_entries())[0].price) self.assertEqual(0.007019, list(result.ask_entries())[0].amount) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_when_subscribing_raised_cancelled( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.huobi.huobi_api_order_book_data_source.HuobiAPIOrderBookDataSource._sleep" ) def test_listen_for_subscriptions_raises_logs_exception( self, sleep_mock, ws_connect_mock): sleep_mock.side_effect = lambda *_: ( # Allows listen_for_subscriptions to yield control over thread self.ev_loop.run_until_complete(asyncio.sleep(0.0))) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = lambda *_: self._create_exception_and_unlock_test_with_event( Exception("TEST ERROR")) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_successful_subbed(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) subbed_message = { "id": self.ex_trading_pair, "status": "ok", "subbed": f"market.{self.ex_trading_pair}.depth.step0", "ts": 1637333566824, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(subbed_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.TRADE_CHANNEL_SUFFIX].qsize()) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_handle_ping_successful( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ping_message = {"ping": 1637333569837} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(ping_message), message_type=aiohttp.WSMsgType.BINARY) # Adds a dummy message to ensure ping message is being handle before breaking from listening task. dummy_message = {"msg": "DUMMY MESSAGE"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(dummy_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.TRADE_CHANNEL_SUFFIX].qsize()) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize()) sent_json: List[Dict[ str, Any]] = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertTrue(any(["pong" in str(payload) for payload in sent_json])) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_successfully_append_trade_and_orderbook_messages( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) trade_message = { "ch": f"market.{self.ex_trading_pair}.trade.detail", "ts": 1630994963175, "tick": { "id": 137005445109, "ts": 1630994963173, "data": [{ "id": 137005445109359286410323766, "ts": 1630994963173, "tradeId": 102523573486, "amount": 0.006754, "price": 52648.62, "direction": "buy", }], }, } orderbook_message = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "ts": 1630983549503, "tick": { "bids": [[52690.69, 0.36281], [52690.68, 0.2]], "asks": [[52690.7, 0.372591], [52691.26, 0.13]], "version": 136998124622, "ts": 1630983549500, }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(trade_message), message_type=aiohttp.WSMsgType.BINARY) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(orderbook_message), message_type=aiohttp.WSMsgType.BINARY, ) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual( 1, self.data_source._message_queue[ self.data_source.TRADE_CHANNEL_SUFFIX].qsize()) self.assertEqual( 1, self.data_source._message_queue[ self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_logs_exception(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) trade_message = { "ch": f"market.{self.ex_trading_pair}.trade.detail", "err": "INCOMPLETE MESSAGE" } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(trade_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) trade_message = { "ch": f"market.{self.ex_trading_pair}.trade.detail", "ts": 1630994963175, "tick": { "id": 137005445109, "ts": 1630994963173, "data": [{ "id": 137005445109359286410323766, "ts": 1630994963173, "tradeId": 102523573486, "amount": 0.006754, "price": 52648.62, "direction": "buy", }], }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(trade_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(1, msg_queue.qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_logs_exception(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) orderbook_message = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "err": "INCOMPLETE MESSAGE" } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(orderbook_message), message_type=aiohttp.WSMsgType.BINARY, ) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) orderbook_message = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "ts": 1630983549503, "tick": { "bids": [[52690.69, 0.36281], [52690.68, 0.2]], "asks": [[52690.7, 0.372591], [52691.26, 0.13]], "version": 136998124622, "ts": 1630983549500, }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(orderbook_message), message_type=aiohttp.WSMsgType.BINARY, ) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(1, msg_queue.qsize()) @aioresponses() @patch( "hummingbot.connector.exchange.huobi.huobi_api_order_book_data_source.HuobiAPIOrderBookDataSource._sleep" ) def test_listen_for_order_book_snapshots_successful(self, mock_api, _): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "status": "ok", "ts": 1637255180894, "tick": { "bids": [ [57069.57, 0.05], ], "asks": [ [57057.73, 0.007019], ], "version": 141982962388, "ts": 1637255180700, }, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) mock_api.get(regex_url, exception=asyncio.CancelledError) msg_queue = asyncio.Queue() # Purposefully raised error to exit task loop with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) result = self.async_run_with_timeout(coroutine=msg_queue.get()) self.assertIsInstance(result, OrderBookMessage)
class CoinflexAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.domain = CONSTANTS.DEFAULT_DOMAIN def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = CoinflexAPIOrderBookDataSource( trading_pairs=[self.trading_pair], throttler=self.throttler, domain=self.domain) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.resume_test_event = asyncio.Event() CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = { "live": bidict({f"{self.ex_trading_pair}": self.trading_pair}) } def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {} super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _successfully_subscribed_event(self): resp = {"result": None, "id": 1} return resp def _login_message(self): resp = { "tag": "1234567890", "event": "login", "success": True, "timestamp": "1234567890" } return resp def _trade_update_event(self): resp = { "table": "trade", "data": [{ "timestamp": 123456789, "marketCode": self.ex_trading_pair, "tradeId": 12345, "side": "BUY", "price": "0.001", "quantity": "100", }] } return resp def _order_diff_event(self): resp = { "table": "depth", "data": [{ "timestamp": 123456789, "instrumentId": self.ex_trading_pair, "seqNum": 157, "bids": [["0.0024", "10"]], "asks": [["0.0026", "100"]] }] } return resp def _snapshot_response(self, update_id=1027024): resp = { "event": "depthL1000", "timestamp": update_id, "data": [{ "bids": [["4.00000000", "431.00000000"]], "asks": [["4.00000200", "12.00000000"]], "marketCode": self.ex_trading_pair, "timestamp": update_id, }] } return resp def _get_regex_url(self, endpoint, return_url=False, endpoint_api_version=None, public=True): prv_or_pub = web_utils.public_rest_url if public else web_utils.private_rest_url url = prv_or_pub(endpoint, domain=self.domain, endpoint_api_version=endpoint_api_version) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) if return_url: return url, regex_url return regex_url @aioresponses() def test_get_last_trade_prices(self, mock_api): url, regex_url = self._get_regex_url( CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, return_url=True) mock_response = [{ "last": "100.0", "open24h": "38719", "high24h": "38840", "low24h": "36377", "volume24h": "3622970.9407847790", "currencyVolume24h": "96.986", "openInterest": "0", "marketCode": "COINALPHA-HBOT", "timestamp": "1645546950025", "lastQty": "0.086", "markPrice": "37645", "lastMarkPrice": "37628", }] mock_api.get(regex_url, body=json.dumps(mock_response)) result: Dict[str, float] = self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair]) @aioresponses() def test_get_last_trade_prices_exception_raised(self, mock_api): url, regex_url = self._get_regex_url( CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, return_url=True) mock_api.get(regex_url, body=json.dumps([{ "marketCode": "COINALPHA-HBOT" }])) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) @aioresponses() def test_get_all_mid_prices(self, mock_api): url = web_utils.public_rest_url( path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=self.domain) mock_response: List[Dict[str, Any]] = [ { # Truncated Response "marketCode": self.ex_trading_pair, "last": "100", }, { # Truncated Response for unrecognized pair "marketCode": "BCC-BTC", "last": "99", } ] mock_api.get(url, body=json.dumps(mock_response)) result: Dict[str, float] = self.async_run_with_timeout( self.data_source.get_all_mid_prices()) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair]) @aioresponses() def test_fetch_trading_pairs(self, mock_api): CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.public_rest_url( path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) mock_response: Dict[str, Any] = { "event": "markets", "timestamp": "1639598493658", "data": [ { "marketId": "2001000000000", "marketCode": "BTC-USD", "name": "BTC/USD", "referencePair": "BTC/USD", "base": "BTC", "counter": "USD", "type": "MARGIN", "tickSize": "1", "qtyIncrement": "0.001", "marginCurrency": "USD", "contractValCurrency": "BTC", "upperPriceBound": "39203", "lowerPriceBound": "36187", "marketPrice": "37695", "markPrice": None, "listingDate": 1593316800000, "endDate": 0, "marketPriceLastUpdated": 1645547473153, "markPriceLastUpdated": 0 }, { "marketId": "34001000000000", "marketCode": "LTC-USD", "name": "LTC/USD", "referencePair": "LTC/USD", "base": "LTC", "counter": "USD", "type": "SPOT", "tickSize": "0.1", "qtyIncrement": "0.01", "marginCurrency": "USD", "contractValCurrency": "LTC", "upperPriceBound": "114.2", "lowerPriceBound": "97.2", "marketPrice": "105.7", "markPrice": None, "listingDate": 1609765200000, "endDate": 0, "marketPriceLastUpdated": 1645547512308, "markPriceLastUpdated": 0 }, { "marketId": "4001000000000", "marketCode": "ETH-USD", "name": "ETH/USD", "referencePair": "ETH/USD", "base": "ETH", "counter": "USD", "type": "SPOT", "tickSize": "0.1", "qtyIncrement": "0.01", "marginCurrency": "USD", "contractValCurrency": "ETH", "upperPriceBound": "2704.3", "lowerPriceBound": "2496.1", "marketPrice": "2600.2", "markPrice": None, "listingDate": 0, "endDate": 0, "marketPriceLastUpdated": 1645547505166, "markPriceLastUpdated": 0 }, ] } mock_api.get(url, body=json.dumps(mock_response)) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(2, len(result)) self.assertIn("ETH-USD", result) self.assertIn("LTC-USD", result) self.assertNotIn("BTC-USD", result) @aioresponses() @patch( "hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time" ) def test_fetch_trading_pairs_exception_raised(self, mock_api, retry_sleep_time_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {} url, regex_url = self._get_regex_url(CONSTANTS.EXCHANGE_INFO_PATH_URL, return_url=True) mock_api.get(regex_url, exception=Exception) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) @aioresponses() def test_get_snapshot_successful(self, mock_api): url, regex_url = self._get_regex_url( CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) result: Dict[str, Any] = self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) self.assertEqual(self._snapshot_response()["data"][0], result) @aioresponses() @patch( "hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time" ) def test_get_snapshot_catch_exception(self, mock_api, retry_sleep_time_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 url, regex_url = self._get_regex_url( CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, status=400) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) mock_api.get(regex_url, body=json.dumps({})) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) @aioresponses() def test_get_new_order_book(self, mock_api): url, regex_url = self._get_regex_url( CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(self._snapshot_response(update_id=1))) result: OrderBook = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertEqual(1, result.snapshot_uid) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(self._login_message())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(1, len(sent_subscription_messages)) expected_subscription = { "op": "subscribe", "args": [ f"trade:{self.ex_trading_pair}", f"depth:{self.ex_trading_pair}", ], } self.assertEqual(expected_subscription, sent_subscription_messages[0]) self.assertTrue( self._is_logged( "INFO", "Subscribed to public order book and trade channels...")) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect") def test_listen_for_subscriptions_raises_cancel_exception( self, mock_ws, _: AsyncMock): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_logs_exception_details( self, mock_ws, sleep_mock): mock_ws.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) def test_subscribe_channels_raises_cancel_exception(self): mock_ws = MagicMock() mock_ws.send.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) def test_subscribe_channels_raises_exception_and_logs_error(self): mock_ws = MagicMock() mock_ws.send.side_effect = Exception("Test Error") with self.assertRaises(Exception): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) def test_listen_for_trades_cancelled_when_listening(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_trades_logs_exception(self): incomplete_resp = { "data": [{ "m": 1, "i": 2, }], } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public trade updates from exchange" )) def test_listen_for_trades_successful(self): mock_queue = AsyncMock() mock_queue.get.side_effect = [ self._login_message(), self._trade_update_event(), asyncio.CancelledError() ] self.data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(-1, msg.update_id) def test_listen_for_order_book_diffs_cancelled(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_order_book_diffs_logs_exception(self): incomplete_resp = { "data": [{ "m": 1, "i": 2, }], } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public order book updates from exchange" )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(self._login_message())) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(self._order_diff_event())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) mock_queue = AsyncMock() mock_queue.get.side_effect = [ self._login_message(), self._order_diff_event(), asyncio.CancelledError() ] self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(123456789, msg.update_id) @aioresponses() def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot( self, mock_api): url, regex_url = self._get_regex_url( CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, exception=asyncio.CancelledError) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_order_book_snapshots( self.ev_loop, asyncio.Queue())) @aioresponses() @patch( "hummingbot.connector.exchange.coinflex.coinflex_api_order_book_data_source" ".CoinflexAPIOrderBookDataSource._sleep") @patch( "hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time" ) def test_listen_for_order_book_snapshots_log_exception( self, mock_api, retry_sleep_time_mock, sleep_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) url, regex_url = self._get_regex_url( CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, exception=Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}." )) @aioresponses() @patch( "hummingbot.connector.exchange.coinflex.coinflex_api_order_book_data_source" ".CoinflexAPIOrderBookDataSource._sleep") def test_listen_for_order_book_snapshots_log_outer_exception( self, mock_api, sleep_mock): msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( Exception("Dummy")) url, regex_url = self._get_regex_url( CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue(self._is_logged("ERROR", "Unexpected error.")) @aioresponses() def test_listen_for_order_book_snapshots_successful( self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() url, regex_url = self._get_regex_url( CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(1027024, msg.update_id)