1
0

test_short_payload_guards.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. """
  2. Regression tests: parsers must not raise on short payloads.
  3. The dispatcher in `switchbot/adv_parser.py` does not pre-validate
  4. `mfr_data` / `data` length before invoking parsers. A malformed BLE
  5. advertisement (untrusted but range-limited input) should degrade to
  6. an empty dict / None values, not raise an `IndexError` / `ValueError`
  7. that bubbles up to callers.
  8. """
  9. from __future__ import annotations
  10. import pytest
  11. from switchbot.adv_parsers.air_purifier import process_air_purifier
  12. from switchbot.adv_parsers.art_frame import process_art_frame
  13. from switchbot.adv_parsers.blind_tilt import process_woblindtilt
  14. from switchbot.adv_parsers.bot import process_wohand
  15. from switchbot.adv_parsers.bulb import process_color_bulb
  16. from switchbot.adv_parsers.ceiling_light import process_woceiling
  17. from switchbot.adv_parsers.climate_panel import process_climate_panel
  18. from switchbot.adv_parsers.curtain import process_wocurtain
  19. from switchbot.adv_parsers.fan import process_fan, process_standing_fan
  20. from switchbot.adv_parsers.hub2 import process_wohub2
  21. from switchbot.adv_parsers.hub3 import process_hub3
  22. from switchbot.adv_parsers.hubmini_matter import process_hubmini_matter
  23. from switchbot.adv_parsers.humidifier import (
  24. process_evaporative_humidifier,
  25. process_wohumidifier,
  26. )
  27. from switchbot.adv_parsers.keypad import process_wokeypad
  28. from switchbot.adv_parsers.keypad_vision import (
  29. process_keypad_vision,
  30. process_keypad_vision_pro,
  31. )
  32. from switchbot.adv_parsers.light_strip import (
  33. process_candle_warmer_lamp,
  34. process_light,
  35. process_rgbic_light,
  36. process_wostrip,
  37. )
  38. from switchbot.adv_parsers.lock import (
  39. parse_common_data,
  40. process_lock2,
  41. process_locklite,
  42. process_wolock,
  43. process_wolock_pro,
  44. )
  45. from switchbot.adv_parsers.meter import process_wosensorth
  46. from switchbot.adv_parsers.motion import process_wopresence
  47. from switchbot.adv_parsers.plug import process_woplugmini
  48. from switchbot.adv_parsers.remote import process_woremote, process_wouniversal_remote
  49. from switchbot.adv_parsers.roller_shade import process_worollershade
  50. from switchbot.adv_parsers.smart_thermostat_radiator import (
  51. process_smart_thermostat_radiator,
  52. )
  53. from switchbot.adv_parsers.vacuum import process_vacuum, process_vacuum_k
  54. EMPTY = b""
  55. SHORT = b"\x00" * 4
  56. @pytest.mark.parametrize(
  57. "parser",
  58. [
  59. process_air_purifier,
  60. process_art_frame,
  61. process_woblindtilt,
  62. process_color_bulb,
  63. process_woceiling,
  64. process_climate_panel,
  65. process_wocurtain,
  66. process_fan,
  67. process_standing_fan,
  68. process_wohub2,
  69. process_hub3,
  70. process_hubmini_matter,
  71. process_evaporative_humidifier,
  72. process_keypad_vision,
  73. process_keypad_vision_pro,
  74. process_wostrip,
  75. process_candle_warmer_lamp,
  76. process_woplugmini,
  77. process_worollershade,
  78. process_smart_thermostat_radiator,
  79. process_vacuum,
  80. process_vacuum_k,
  81. ],
  82. )
  83. @pytest.mark.parametrize("payload", [None, EMPTY, SHORT])
  84. def test_mfr_only_parsers_return_empty_on_short(parser, payload):
  85. """Parsers that read only mfr_data must return {} for short payloads."""
  86. assert parser(None, payload) == {}
  87. @pytest.mark.parametrize("payload", [None, EMPTY, SHORT, b"\x00" * 17])
  88. def test_process_light_short_payload(payload):
  89. """process_light needs cw_offset + 2 bytes (default 18)."""
  90. assert process_light(None, payload) == {}
  91. @pytest.mark.parametrize("payload", [None, EMPTY, SHORT, b"\x00" * 11])
  92. def test_process_rgbic_light_short_payload(payload):
  93. """process_rgbic_light uses cw_offset=10, so needs >= 12 bytes."""
  94. assert process_rgbic_light(None, payload) == {}
  95. @pytest.mark.parametrize(
  96. ("data", "mfr_data"),
  97. [
  98. (None, None),
  99. (EMPTY, None),
  100. (b"\x00\x00", None),
  101. ],
  102. )
  103. def test_process_wohand_short_data(data, mfr_data):
  104. """process_wohand must not crash on short `data`."""
  105. out = process_wohand(data, mfr_data)
  106. # Either an empty dict (both None) or all-None values
  107. assert "isOn" in out or out == {}
  108. assert out.get("battery") in (None, *out.values())
  109. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
  110. def test_process_woremote_short_data(data):
  111. out = process_woremote(data, None)
  112. assert out == {"battery": None}
  113. @pytest.mark.parametrize(
  114. "mfr_data",
  115. [None, EMPTY, b"\x00", b"\x00" * 7],
  116. )
  117. def test_process_wouniversal_remote_short_mfr(mfr_data):
  118. out = process_wouniversal_remote(None, mfr_data)
  119. assert out == {"battery": None, "charging": None}
  120. @pytest.mark.parametrize(
  121. ("data", "mfr_data"),
  122. [
  123. (None, None),
  124. (b"\x00", None),
  125. (None, b"\x00"),
  126. (b"\x00\x00", b"\x00\x00"),
  127. ],
  128. )
  129. def test_process_wokeypad_short(data, mfr_data):
  130. out = process_wokeypad(data, mfr_data)
  131. assert out == {"battery": None, "attempt_state": None}
  132. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
  133. def test_process_wohumidifier_short_data(data):
  134. out = process_wohumidifier(data, None)
  135. assert out == {"isOn": None, "level": None, "switchMode": True}
  136. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
  137. @pytest.mark.parametrize("mfr_data", [None, EMPTY, b"\x00\x00\x00"])
  138. def test_process_wosensorth_short(data, mfr_data):
  139. """process_wosensorth must not crash; returns {} when no usable payload."""
  140. out = process_wosensorth(data, mfr_data)
  141. assert isinstance(out, dict)
  142. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
  143. @pytest.mark.parametrize("mfr_data", [None, EMPTY, b"\x00\x00\x00"])
  144. def test_process_wopresence_short(data, mfr_data):
  145. """process_wopresence must not crash even when both inputs are short."""
  146. out = process_wopresence(data, mfr_data)
  147. assert isinstance(out, dict)
  148. @pytest.mark.parametrize("mfr_data", [None, EMPTY, SHORT])
  149. def test_lock_parsers_short_mfr(mfr_data):
  150. assert process_locklite(None, mfr_data) == {}
  151. assert process_wolock(None, mfr_data) == {}
  152. assert parse_common_data(mfr_data) == {}
  153. assert process_wolock_pro(None, mfr_data) == {}
  154. assert process_lock2(None, mfr_data) == {}
  155. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
  156. def test_blind_tilt_short_data(data):
  157. """blind_tilt with full mfr but short data must not crash on data[2]."""
  158. mfr = b"\x00" * 10
  159. out = process_woblindtilt(data, mfr)
  160. assert out["battery"] is None
  161. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
  162. def test_curtain_short_data_with_long_mfr(data):
  163. """Curtain >=11 path uses data[2] for battery; must not crash on short data."""
  164. mfr = b"\x00" * 11
  165. out = process_wocurtain(data, mfr)
  166. assert out["battery"] is None
  167. @pytest.mark.parametrize("data", [b"\x00\x00\x00\x00", b"\x00" * 5])
  168. def test_curtain_short_data_only(data):
  169. """Curtain data-only path needs len >= 6; shorter -> {}."""
  170. assert process_wocurtain(data, None) == {}
  171. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
  172. def test_roller_shade_short_data(data):
  173. """roller_shade with short data must not crash on data[2]."""
  174. mfr = b"\x00" * 10
  175. out = process_worollershade(data, mfr)
  176. assert out["battery"] is None