From a1dd348c73b69e7a53c899683e8ee461bc275571 Mon Sep 17 00:00:00 2001 From: Maximilian Rink Date: Mon, 12 Oct 2026 07:07:50 +0200 Subject: [PATCH 1/2] Raise decode error for non-object miIO payloads --- miio/miioprotocol.py | 7 +++++++ miio/tests/test_protocol.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) 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..68c62d248 100644 --- a/miio/tests/test_protocol.py +++ b/miio/tests/test_protocol.py @@ -1,4 +1,6 @@ import binascii +from types import SimpleNamespace +from unittest.mock import patch import pytest @@ -169,3 +171,23 @@ 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) + + +def test_send_raises_payload_decode_for_raw_bytes(): + """Malformed decrypted payloads must not escape as a TypeError.""" + proto = MiIOProtocol("127.0.0.1") + proto._discovered = True + proto._device_id = b"\x01\x02\x03\x04" + response = SimpleNamespace( + header=SimpleNamespace(value={"ts": 0}), + data=SimpleNamespace(value=b"not-json"), + ) + + with ( + patch.object(proto, "_create_request", return_value={"id": 1}), + patch("socket.socket") as mock_socket, + patch("miio.miioprotocol.Message.parse", return_value=response), + ): + mock_socket.return_value.recvfrom.return_value = (b"response", ("127.0.0.1", 54321)) + with pytest.raises(PayloadDecodeException, match="expected an object"): + proto.send("test", retry_count=0) From 32f7a52abcdf1b8c57d8d9fd116b324c8872a8a4 Mon Sep 17 00:00:00 2001 From: Maximilian Rink Date: Mon, 12 Oct 2026 07:09:56 +0200 Subject: [PATCH 2/2] Test non-object miIO responses --- miio/tests/test_protocol.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/miio/tests/test_protocol.py b/miio/tests/test_protocol.py index 68c62d248..ea7739974 100644 --- a/miio/tests/test_protocol.py +++ b/miio/tests/test_protocol.py @@ -1,5 +1,4 @@ import binascii -from types import SimpleNamespace from unittest.mock import patch import pytest @@ -173,21 +172,39 @@ def test_decode_json_raises_for_invalid_json(token): Message.parse(serialized_msg, **ctx) -def test_send_raises_payload_decode_for_raw_bytes(): - """Malformed decrypted payloads must not escape as a TypeError.""" +@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 = SimpleNamespace( - header=SimpleNamespace(value={"ts": 0}), - data=SimpleNamespace(value=b"not-json"), - ) + response = build_msg(payload, token) with ( patch.object(proto, "_create_request", return_value={"id": 1}), patch("socket.socket") as mock_socket, - patch("miio.miioprotocol.Message.parse", return_value=response), ): - mock_socket.return_value.recvfrom.return_value = (b"response", ("127.0.0.1", 54321)) + 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"]