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)
    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_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])
    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..."
            ))
    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)
    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))
    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])
    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
    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()))
    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))
    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()
    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)
    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}."
            ))
    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())
    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..."))
    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)
    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..."
            ))