|
| 1 | +import asyncio |
1 | 2 | import json |
2 | 3 | from collections.abc import AsyncGenerator |
3 | 4 | from queue import Queue |
|
15 | 16 | UserData, |
16 | 17 | ) |
17 | 18 | from roborock.containers import DeviceData, RoomMapping, S7MaxVStatus |
18 | | -from roborock.exceptions import RoborockException |
| 19 | +from roborock.exceptions import RoborockException, RoborockTimeout |
19 | 20 | from roborock.protocol import MessageParser |
20 | 21 | from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol |
21 | 22 | from roborock.version_1_apis import RoborockMqttClientV1 |
@@ -154,8 +155,6 @@ async def connected_mqtt_client_fixture( |
154 | 155 | response_queue.put(mqtt_packet.gen_suback(1, 0)) |
155 | 156 | await mqtt_client.async_connect() |
156 | 157 | yield mqtt_client |
157 | | - if mqtt_client.is_connected(): |
158 | | - await mqtt_client.async_disconnect() |
159 | 158 |
|
160 | 159 |
|
161 | 160 | async def test_async_connect(received_requests: Queue, connected_mqtt_client: RoborockMqttClientV1) -> None: |
@@ -187,6 +186,58 @@ async def test_connect_failure( |
187 | 186 | assert received_requests.qsize() == 1 # Connect attempt |
188 | 187 |
|
189 | 188 |
|
| 189 | +async def test_disconnect_already_disconnected(connected_mqtt_client: RoborockMqttClientV1) -> None: |
| 190 | + """Test the MQTT client error handling for a no-op disconnect.""" |
| 191 | + |
| 192 | + assert connected_mqtt_client.is_connected() |
| 193 | + |
| 194 | + # Make the MQTT client simulate returning that it already thinks it is disconnected |
| 195 | + with patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_NO_CONN): |
| 196 | + await connected_mqtt_client.async_disconnect() |
| 197 | + |
| 198 | + |
| 199 | +async def test_disconnect_failure(connected_mqtt_client: RoborockMqttClientV1) -> None: |
| 200 | + """Test that the MQTT client ignores MQTT client error handling for a no-op disconnect.""" |
| 201 | + |
| 202 | + assert connected_mqtt_client.is_connected() |
| 203 | + |
| 204 | + # Make the MQTT client returns with an error when disconnecting |
| 205 | + with patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_PROTOCOL), pytest.raises( |
| 206 | + RoborockException, match="Failed to disconnect" |
| 207 | + ): |
| 208 | + await connected_mqtt_client.async_disconnect() |
| 209 | + |
| 210 | + |
| 211 | +async def test_async_release(connected_mqtt_client: RoborockMqttClientV1) -> None: |
| 212 | + """Test the async_release API will disconnect the client.""" |
| 213 | + await connected_mqtt_client.async_release() |
| 214 | + assert not connected_mqtt_client.is_connected() |
| 215 | + |
| 216 | + |
| 217 | +async def test_subscribe_failure( |
| 218 | + received_requests: Queue, response_queue: Queue, mqtt_client: RoborockMqttClientV1 |
| 219 | +) -> None: |
| 220 | + """Test the broker responding with the wrong message type on subscribe.""" |
| 221 | + |
| 222 | + response_queue.put(mqtt_packet.gen_connack(rc=0, flags=2)) |
| 223 | + |
| 224 | + with patch("roborock.cloud_api.mqtt.Client.subscribe", return_value=(mqtt.MQTT_ERR_NO_CONN, None)), pytest.raises( |
| 225 | + RoborockException, match="Failed to subscribe" |
| 226 | + ): |
| 227 | + await mqtt_client.async_connect() |
| 228 | + |
| 229 | + assert received_requests.qsize() == 1 # Connect attempt |
| 230 | + |
| 231 | + # NOTE: The client is "connected" but not "subscribed" and cannot recover |
| 232 | + # from this state without disconnecting first. This can likely be improved. |
| 233 | + assert mqtt_client.is_connected() |
| 234 | + |
| 235 | + # Attempting to reconnect is a no-op since the client already thinks it is connected |
| 236 | + await mqtt_client.async_connect() |
| 237 | + assert mqtt_client.is_connected() |
| 238 | + assert received_requests.qsize() == 1 |
| 239 | + |
| 240 | + |
190 | 241 | def build_rpc_response(message: dict[str, Any]) -> bytes: |
191 | 242 | """Build an encoded RPC response message.""" |
192 | 243 | return MessageParser.build( |
@@ -228,3 +279,26 @@ async def test_get_room_mapping( |
228 | 279 | RoomMapping(segment_id=16, iot_id="2362048"), |
229 | 280 | RoomMapping(segment_id=17, iot_id="2362044"), |
230 | 281 | ] |
| 282 | + |
| 283 | + |
| 284 | +async def test_publish_failure( |
| 285 | + connected_mqtt_client: RoborockMqttClientV1, |
| 286 | +) -> None: |
| 287 | + """Test a failure return code when publishing a messaage.""" |
| 288 | + |
| 289 | + msg = mqtt.MQTTMessageInfo(0) |
| 290 | + msg.rc = mqtt.MQTT_ERR_PROTOCOL |
| 291 | + with patch("roborock.cloud_api.mqtt.Client.publish", return_value=msg), pytest.raises( |
| 292 | + RoborockException, match="Failed to publish" |
| 293 | + ): |
| 294 | + await connected_mqtt_client.get_room_mapping() |
| 295 | + |
| 296 | + |
| 297 | +async def test_future_timeout( |
| 298 | + connected_mqtt_client: RoborockMqttClientV1, |
| 299 | +) -> None: |
| 300 | + """Test a timeout raised while waiting for an RPC response.""" |
| 301 | + with patch("roborock.roborock_future.async_timeout.timeout", side_effect=asyncio.TimeoutError), pytest.raises( |
| 302 | + RoborockTimeout, match="Timeout after" |
| 303 | + ): |
| 304 | + await connected_mqtt_client.get_room_mapping() |
0 commit comments