File: test_protocol.py

package info (click to toggle)
python-miio 0.5.12-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,888 kB
  • sloc: python: 23,425; makefile: 9
file content (160 lines) | stat: -rw-r--r-- 4,574 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import binascii

import pytest

from miio.exceptions import DeviceError, PayloadDecodeException, RecoverableError

from .. import Utils
from ..miioprotocol import MiIOProtocol
from ..protocol import Message

METHOD = "method"
PARAMS = "params"


@pytest.fixture
def proto() -> MiIOProtocol:
    return MiIOProtocol()


@pytest.fixture
def token() -> bytes:
    return bytes.fromhex(32 * "0")


def build_msg(data, token):
    encrypted_data = Utils.encrypt(data, token)

    # header
    magic = binascii.unhexlify(b"2131")
    length = (32 + len(encrypted_data)).to_bytes(2, byteorder="big")
    unknown = binascii.unhexlify(b"00000000")
    did = binascii.unhexlify(b"01234567")
    epoch = binascii.unhexlify(b"00000000")

    checksum = Utils.md5(
        magic + length + unknown + did + epoch + token + encrypted_data
    )

    return magic + length + unknown + did + epoch + checksum + encrypted_data


def test_incrementing_id(proto):
    old_id = proto.raw_id
    proto._create_request("dummycmd", "dummy")
    assert proto.raw_id > old_id


def test_id_loop(proto):
    proto.__id = 9999
    proto._create_request("dummycmd", "dummy")
    assert proto.raw_id == 1


def test_request_with_none_param(proto):
    req = proto._create_request("dummy", None)
    assert isinstance(req["params"], list)
    assert len(req["params"]) == 0


def test_request_with_string_param(proto):
    req = proto._create_request("command", "single")
    assert req[METHOD] == "command"
    assert req[PARAMS] == "single"


def test_request_with_list_param(proto):
    req = proto._create_request("command", ["item"])
    assert req[METHOD] == "command"
    assert req[PARAMS] == ["item"]


def test_request_extra_params(proto):
    req = proto._create_request("command", ["item"], extra_parameters={"sid": 1234})
    assert "sid" in req
    assert req["sid"] == 1234


@pytest.mark.parametrize("retry_error", [-30001, -9999])
def test_device_error_handling(proto: MiIOProtocol, retry_error):
    with pytest.raises(RecoverableError):
        proto._handle_error({"code": retry_error})

    with pytest.raises(DeviceError):
        proto._handle_error({"code": 1234})


def test_non_bytes_payload(token):
    payload = "hello world"
    with pytest.raises(TypeError):
        Utils.encrypt(payload, token)
    with pytest.raises(TypeError):
        Utils.decrypt(payload, token)


def test_encrypt(token):
    payload = b"hello world"

    encrypted = Utils.encrypt(payload, token)
    decrypted = Utils.decrypt(encrypted, token)
    assert payload == decrypted


def test_invalid_token():
    payload = b"hello world"
    wrong_type = 1234
    wrong_length = bytes.fromhex(16 * "0")
    with pytest.raises(TypeError):
        Utils.encrypt(payload, wrong_type)
    with pytest.raises(TypeError):
        Utils.decrypt(payload, wrong_type)

    with pytest.raises(ValueError):
        Utils.encrypt(payload, wrong_length)
    with pytest.raises(ValueError):
        Utils.decrypt(payload, wrong_length)


def test_decode_json_payload(token):
    ctx = {"token": token}

    # can parse message with valid json
    serialized_msg = build_msg(b'{"id": 123456}', token)
    parsed_msg = Message.parse(serialized_msg, **ctx)
    assert parsed_msg.data.value
    assert isinstance(parsed_msg.data.value, dict)
    assert parsed_msg.data.value["id"] == 123456


def test_decode_json_quirk_powerstrip(token):
    ctx = {"token": token}

    # can parse message with invalid json for edge case powerstrip
    # when not connected to cloud
    serialized_msg = build_msg(b'{"id": 123456,,"otu_stat":0}', token)
    parsed_msg = Message.parse(serialized_msg, **ctx)
    assert parsed_msg.data.value
    assert isinstance(parsed_msg.data.value, dict)
    assert parsed_msg.data.value["id"] == 123456
    assert parsed_msg.data.value["otu_stat"] == 0


def test_decode_json_quirk_cloud(token):
    ctx = {"token": token}

    # can parse message with invalid json for edge case xiaomi cloud
    # reply to _sync.batch_gen_room_up_url
    serialized_msg = build_msg(b'{"id": 123456}\x00k', token)
    parsed_msg = Message.parse(serialized_msg, **ctx)
    assert parsed_msg.data.value
    assert isinstance(parsed_msg.data.value, dict)
    assert parsed_msg.data.value["id"] == 123456


def test_decode_json_raises_for_invalid_json(token):
    ctx = {"token": token}

    # make sure PayloadDecodeDexception is raised for invalid json
    serialized_msg = build_msg(b'{"id": 123456,,"otu_stat":0', token)
    with pytest.raises(PayloadDecodeException):
        Message.parse(serialized_msg, **ctx)