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)
    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 __init__(
         self,
         huobi_auth: Optional[HuobiAuth] = None,
         api_factory: Optional[WebAssistantsFactory] = None,
 ):
     self._huobi_auth: HuobiAuth = huobi_auth
     self._api_factory = api_factory
     super().__init__(data_source=HuobiAPIUserStreamDataSource(
         huobi_auth=self._huobi_auth,
         api_factory=self._api_factory))
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())
 def data_source(self) -> UserStreamTrackerDataSource:
     if not self._data_source:
         self._data_source = HuobiAPIUserStreamDataSource(huobi_auth=self._huobi_auth,
                                                          api_factory=self._api_factory)
     return self._data_source