diff --git a/miio/miioprotocol.py b/miio/miioprotocol.py index edb657562..c9003bed5 100644 --- a/miio/miioprotocol.py +++ b/miio/miioprotocol.py @@ -8,6 +8,7 @@ import codecs import logging import socket +from collections.abc import Mapping from datetime import UTC, datetime, timedelta from pprint import pformat as pf from typing import Any @@ -18,6 +19,7 @@ DeviceError, DeviceException, InvalidTokenException, + PayloadDecodeException, RecoverableError, ) from .protocol import Message @@ -247,6 +249,11 @@ def send( header = m.header.value payload = m.data.value + if not isinstance(payload, Mapping): + raise PayloadDecodeException( + "Unable to parse message payload: expected an object" + ) + self.__id = payload["id"] self._device_ts = header["ts"] # type: ignore # ts uses timeadapter diff --git a/miio/tests/test_protocol.py b/miio/tests/test_protocol.py index 184ac4103..ea7739974 100644 --- a/miio/tests/test_protocol.py +++ b/miio/tests/test_protocol.py @@ -1,4 +1,5 @@ import binascii +from unittest.mock import patch import pytest @@ -169,3 +170,41 @@ def test_decode_json_raises_for_invalid_json(token): serialized_msg = build_msg(b'{"id": 123456,,"otu_stat":0', token) with pytest.raises(PayloadDecodeException): Message.parse(serialized_msg, **ctx) + + +@pytest.mark.parametrize("payload", [b"[]", b"null", b'"not-an-object"']) +def test_send_raises_payload_decode_for_non_object_payload(payload, token): + """Valid JSON values must not escape as a TypeError.""" + proto = MiIOProtocol("127.0.0.1") + proto._discovered = True + proto._device_id = b"\x01\x02\x03\x04" + response = build_msg(payload, token) + + with ( + patch.object(proto, "_create_request", return_value={"id": 1}), + patch("socket.socket") as mock_socket, + ): + mock_socket.return_value.recvfrom.return_value = ( + response, + ("127.0.0.1", 54321), + ) + with pytest.raises(PayloadDecodeException, match="expected an object"): + proto.send("test", retry_count=0) + + +def test_send_accepts_mapping_payload(token): + """Object responses continue through the normal result handling path.""" + proto = MiIOProtocol("127.0.0.1") + proto._discovered = True + proto._device_id = b"\x01\x02\x03\x04" + response = build_msg(b'{"id": 7, "result": ["ok"]}', token) + + with ( + patch.object(proto, "_create_request", return_value={"id": 1}), + patch("socket.socket") as mock_socket, + ): + mock_socket.return_value.recvfrom.return_value = ( + response, + ("127.0.0.1", 54321), + ) + assert proto.send("test", retry_count=0) == ["ok"]