test_short_payload_guards.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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
  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. ("data", "mfr_data"),
  115. [
  116. (None, None),
  117. (b"\x00", None),
  118. (None, b"\x00"),
  119. (b"\x00\x00", b"\x00\x00"),
  120. ],
  121. )
  122. def test_process_wokeypad_short(data, mfr_data):
  123. out = process_wokeypad(data, mfr_data)
  124. assert out == {"battery": None, "attempt_state": None}
  125. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
  126. def test_process_wohumidifier_short_data(data):
  127. out = process_wohumidifier(data, None)
  128. assert out == {"isOn": None, "level": None, "switchMode": True}
  129. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
  130. @pytest.mark.parametrize("mfr_data", [None, EMPTY, b"\x00\x00\x00"])
  131. def test_process_wosensorth_short(data, mfr_data):
  132. """process_wosensorth must not crash; returns {} when no usable payload."""
  133. out = process_wosensorth(data, mfr_data)
  134. assert isinstance(out, dict)
  135. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00\x00"])
  136. @pytest.mark.parametrize("mfr_data", [None, EMPTY, b"\x00\x00\x00"])
  137. def test_process_wopresence_short(data, mfr_data):
  138. """process_wopresence must not crash even when both inputs are short."""
  139. out = process_wopresence(data, mfr_data)
  140. assert isinstance(out, dict)
  141. @pytest.mark.parametrize("mfr_data", [None, EMPTY, SHORT])
  142. def test_lock_parsers_short_mfr(mfr_data):
  143. assert process_locklite(None, mfr_data) == {}
  144. assert process_wolock(None, mfr_data) == {}
  145. assert parse_common_data(mfr_data) == {}
  146. assert process_wolock_pro(None, mfr_data) == {}
  147. assert process_lock2(None, mfr_data) == {}
  148. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
  149. def test_blind_tilt_short_data(data):
  150. """blind_tilt with full mfr but short data must not crash on data[2]."""
  151. mfr = b"\x00" * 10
  152. out = process_woblindtilt(data, mfr)
  153. assert out["battery"] is None
  154. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
  155. def test_curtain_short_data_with_long_mfr(data):
  156. """Curtain >=11 path uses data[2] for battery; must not crash on short data."""
  157. mfr = b"\x00" * 11
  158. out = process_wocurtain(data, mfr)
  159. assert out["battery"] is None
  160. @pytest.mark.parametrize("data", [b"\x00\x00\x00\x00", b"\x00" * 5])
  161. def test_curtain_short_data_only(data):
  162. """Curtain data-only path needs len >= 6; shorter -> {}."""
  163. assert process_wocurtain(data, None) == {}
  164. @pytest.mark.parametrize("data", [None, EMPTY, b"\x00\x00"])
  165. def test_roller_shade_short_data(data):
  166. """roller_shade with short data must not crash on data[2]."""
  167. mfr = b"\x00" * 10
  168. out = process_worollershade(data, mfr)
  169. assert out["battery"] is None