|
|
@@ -0,0 +1,202 @@
|
|
|
+"""
|
|
|
+Regression tests: parsers must not raise on short payloads.
|
|
|
+
|
|
|
+The dispatcher in `switchbot/adv_parser.py` does not pre-validate
|
|
|
+`mfr_data` / `data` length before invoking parsers. A malformed BLE
|
|
|
+advertisement (untrusted but range-limited input) should degrade to
|
|
|
+an empty dict / None values, not raise an `IndexError` / `ValueError`
|
|
|
+that bubbles up to callers.
|
|
|
+"""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import pytest
|
|
|
+
|
|
|
+from switchbot.adv_parsers.air_purifier import process_air_purifier
|
|
|
+from switchbot.adv_parsers.art_frame import process_art_frame
|
|
|
+from switchbot.adv_parsers.blind_tilt import process_woblindtilt
|
|
|
+from switchbot.adv_parsers.bot import process_wohand
|
|
|
+from switchbot.adv_parsers.bulb import process_color_bulb
|
|
|
+from switchbot.adv_parsers.ceiling_light import process_woceiling
|
|
|
+from switchbot.adv_parsers.climate_panel import process_climate_panel
|
|
|
+from switchbot.adv_parsers.curtain import process_wocurtain
|
|
|
+from switchbot.adv_parsers.fan import process_fan, process_standing_fan
|
|
|
+from switchbot.adv_parsers.hub2 import process_wohub2
|
|
|
+from switchbot.adv_parsers.hub3 import process_hub3
|
|
|
+from switchbot.adv_parsers.hubmini_matter import process_hubmini_matter
|
|
|
+from switchbot.adv_parsers.humidifier import (
|
|
|
+ process_evaporative_humidifier,
|
|
|
+ process_wohumidifier,
|
|
|
+)
|
|
|
+from switchbot.adv_parsers.keypad import process_wokeypad
|
|
|
+from switchbot.adv_parsers.keypad_vision import (
|
|
|
+ process_keypad_vision,
|
|
|
+ process_keypad_vision_pro,
|
|
|
+)
|
|
|
+from switchbot.adv_parsers.light_strip import (
|
|
|
+ process_candle_warmer_lamp,
|
|
|
+ process_light,
|
|
|
+ process_rgbic_light,
|
|
|
+ process_wostrip,
|
|
|
+)
|
|
|
+from switchbot.adv_parsers.lock import (
|
|
|
+ parse_common_data,
|
|
|
+ process_lock2,
|
|
|
+ process_locklite,
|
|
|
+ process_wolock,
|
|
|
+ process_wolock_pro,
|
|
|
+)
|
|
|
+from switchbot.adv_parsers.meter import process_wosensorth
|
|
|
+from switchbot.adv_parsers.motion import process_wopresence
|
|
|
+from switchbot.adv_parsers.plug import process_woplugmini
|
|
|
+from switchbot.adv_parsers.remote import process_woremote
|
|
|
+from switchbot.adv_parsers.roller_shade import process_worollershade
|
|
|
+from switchbot.adv_parsers.smart_thermostat_radiator import (
|
|
|
+ process_smart_thermostat_radiator,
|
|
|
+)
|
|
|
+from switchbot.adv_parsers.vacuum import process_vacuum, process_vacuum_k
|
|
|
+
|
|
|
+EMPTY = b""
|
|
|
+SHORT = b"\x00" * 4
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "parser",
|
|
|
+ [
|
|
|
+ process_air_purifier,
|
|
|
+ process_art_frame,
|
|
|
+ process_woblindtilt,
|
|
|
+ process_color_bulb,
|
|
|
+ process_woceiling,
|
|
|
+ process_climate_panel,
|
|
|
+ process_wocurtain,
|
|
|
+ process_fan,
|
|
|
+ process_standing_fan,
|
|
|
+ process_wohub2,
|
|
|
+ process_hub3,
|
|
|
+ process_hubmini_matter,
|
|
|
+ process_evaporative_humidifier,
|
|
|
+ process_keypad_vision,
|
|
|
+ process_keypad_vision_pro,
|
|
|
+ process_wostrip,
|
|
|
+ process_candle_warmer_lamp,
|
|
|
+ process_woplugmini,
|
|
|
+ process_worollershade,
|
|
|
+ process_smart_thermostat_radiator,
|
|
|
+ process_vacuum,
|
|
|
+ process_vacuum_k,
|
|
|
+ ],
|
|
|
+)
|
|
|
+@pytest.mark.parametrize("payload", [None, EMPTY, SHORT])
|
|
|
+def test_mfr_only_parsers_return_empty_on_short(parser, payload):
|
|
|
+ """Parsers that read only mfr_data must return {} for short payloads."""
|
|
|
+ assert parser(None, payload) == {}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("payload", [None, EMPTY, SHORT, b"\x00" * 17])
|
|
|
+def test_process_light_short_payload(payload):
|
|
|
+ """process_light needs cw_offset + 2 bytes (default 18)."""
|
|
|
+ assert process_light(None, payload) == {}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("payload", [None, EMPTY, SHORT, b"\x00" * 11])
|
|
|
+def test_process_rgbic_light_short_payload(payload):
|
|
|
+ """process_rgbic_light uses cw_offset=10, so needs >= 12 bytes."""
|
|
|
+ assert process_rgbic_light(None, payload) == {}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("data", "mfr_data"),
|
|
|
+ [
|
|
|
+ (None, None),
|
|
|
+ (EMPTY, None),
|
|
|
+ (b"\x00\x00", None),
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_process_wohand_short_data(data, mfr_data):
|
|
|
+ """process_wohand must not crash on short `data`."""
|
|
|
+ out = process_wohand(data, mfr_data)
|
|
|
+ # Either an empty dict (both None) or all-None values
|
|
|
+ assert "isOn" in out or out == {}
|
|
|
+ assert out.get("battery") in (None, *out.values())
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
|
|
|
+def test_process_woremote_short_data(data):
|
|
|
+ out = process_woremote(data, None)
|
|
|
+ assert out == {"battery": None}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("data", "mfr_data"),
|
|
|
+ [
|
|
|
+ (None, None),
|
|
|
+ (b"\x00", None),
|
|
|
+ (None, b"\x00"),
|
|
|
+ (b"\x00\x00", b"\x00\x00"),
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_process_wokeypad_short(data, mfr_data):
|
|
|
+ out = process_wokeypad(data, mfr_data)
|
|
|
+ assert out == {"battery": None, "attempt_state": None}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
|
|
|
+def test_process_wohumidifier_short_data(data):
|
|
|
+ out = process_wohumidifier(data, None)
|
|
|
+ assert out == {"isOn": None, "level": None, "switchMode": True}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
|
|
|
+@pytest.mark.parametrize("mfr_data", [None, EMPTY, b"\x00\x00\x00"])
|
|
|
+def test_process_wosensorth_short(data, mfr_data):
|
|
|
+ """process_wosensorth must not crash; returns {} when no usable payload."""
|
|
|
+ out = process_wosensorth(data, mfr_data)
|
|
|
+ assert isinstance(out, dict)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
|
|
|
+@pytest.mark.parametrize("mfr_data", [None, EMPTY, b"\x00\x00\x00"])
|
|
|
+def test_process_wopresence_short(data, mfr_data):
|
|
|
+ """process_wopresence must not crash even when both inputs are short."""
|
|
|
+ out = process_wopresence(data, mfr_data)
|
|
|
+ assert isinstance(out, dict)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("mfr_data", [None, EMPTY, SHORT])
|
|
|
+def test_lock_parsers_short_mfr(mfr_data):
|
|
|
+ assert process_locklite(None, mfr_data) == {}
|
|
|
+ assert process_wolock(None, mfr_data) == {}
|
|
|
+ assert parse_common_data(mfr_data) == {}
|
|
|
+ assert process_wolock_pro(None, mfr_data) == {}
|
|
|
+ assert process_lock2(None, mfr_data) == {}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
|
|
|
+def test_blind_tilt_short_data(data):
|
|
|
+ """blind_tilt with full mfr but short data must not crash on data[2]."""
|
|
|
+ mfr = b"\x00" * 10
|
|
|
+ out = process_woblindtilt(data, mfr)
|
|
|
+ assert out["battery"] is None
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
|
|
|
+def test_curtain_short_data_with_long_mfr(data):
|
|
|
+ """Curtain >=11 path uses data[2] for battery; must not crash on short data."""
|
|
|
+ mfr = b"\x00" * 11
|
|
|
+ out = process_wocurtain(data, mfr)
|
|
|
+ assert out["battery"] is None
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [b"\x00\x00\x00\x00", b"\x00" * 5])
|
|
|
+def test_curtain_short_data_only(data):
|
|
|
+ """Curtain data-only path needs len >= 6; shorter -> {}."""
|
|
|
+ assert process_wocurtain(data, None) == {}
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
|
|
|
+def test_roller_shade_short_data(data):
|
|
|
+ """roller_shade with short data must not crash on data[2]."""
|
|
|
+ mfr = b"\x00" * 10
|
|
|
+ out = process_worollershade(data, mfr)
|
|
|
+ assert out["battery"] is None
|