test_device.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. """Tests for device.py functionality."""
  2. from __future__ import annotations
  3. import logging
  4. from typing import Any
  5. from unittest.mock import MagicMock, patch
  6. import aiohttp
  7. import pytest
  8. from switchbot import fetch_cloud_devices
  9. from switchbot.adv_parser import _MODEL_TO_MAC_CACHE, populate_model_to_mac_cache
  10. from switchbot.const import (
  11. SwitchbotAccountConnectionError,
  12. SwitchbotAuthenticationError,
  13. SwitchbotModel,
  14. )
  15. from switchbot.devices.device import SwitchbotBaseDevice, _extract_region
  16. @pytest.fixture
  17. def mock_auth_response() -> dict[str, Any]:
  18. """Mock authentication response."""
  19. return {
  20. "access_token": "test_token_123",
  21. "refresh_token": "refresh_token_123",
  22. "expires_in": 3600,
  23. }
  24. @pytest.fixture
  25. def mock_user_info() -> dict[str, Any]:
  26. """Mock user info response."""
  27. return {
  28. "botRegion": "us",
  29. "country": "us",
  30. "email": "test@example.com",
  31. }
  32. @pytest.fixture
  33. def mock_device_response() -> dict[str, Any]:
  34. """Mock device list response."""
  35. return {
  36. "Items": [
  37. {
  38. "device_mac": "aabbccddeeff",
  39. "device_name": "Test Bot",
  40. "device_detail": {
  41. "device_type": "WoHand",
  42. "version": "1.0.0",
  43. },
  44. },
  45. {
  46. "device_mac": "112233445566",
  47. "device_name": "Test Curtain",
  48. "device_detail": {
  49. "device_type": "WoCurtain",
  50. "version": "2.0.0",
  51. },
  52. },
  53. {
  54. "device_mac": "778899aabbcc",
  55. "device_name": "Test Lock",
  56. "device_detail": {
  57. "device_type": "WoLock",
  58. "version": "1.5.0",
  59. },
  60. },
  61. {
  62. "device_mac": "ddeeff001122",
  63. "device_name": "Unknown Device",
  64. "device_detail": {
  65. "device_type": "WoUnknown",
  66. "version": "1.0.0",
  67. "extra_field": "extra_value",
  68. },
  69. },
  70. {
  71. "device_mac": "invalid_device",
  72. # Missing device_detail
  73. },
  74. {
  75. "device_mac": "another_invalid",
  76. "device_detail": {
  77. # Missing device_type
  78. "version": "1.0.0",
  79. },
  80. },
  81. ]
  82. }
  83. @pytest.mark.asyncio
  84. async def test_get_devices(
  85. mock_auth_response: dict[str, Any],
  86. mock_user_info: dict[str, Any],
  87. mock_device_response: dict[str, Any],
  88. caplog: pytest.LogCaptureFixture,
  89. ) -> None:
  90. """Test get_devices method."""
  91. caplog.set_level(logging.DEBUG)
  92. with (
  93. patch.object(
  94. SwitchbotBaseDevice, "_get_auth_result", return_value=mock_auth_response
  95. ),
  96. patch.object(
  97. SwitchbotBaseDevice, "_async_get_user_info", return_value=mock_user_info
  98. ),
  99. patch.object(
  100. SwitchbotBaseDevice, "api_request", return_value=mock_device_response
  101. ) as mock_api_request,
  102. patch(
  103. "switchbot.devices.device.populate_model_to_mac_cache"
  104. ) as mock_populate_cache,
  105. ):
  106. session = MagicMock(spec=aiohttp.ClientSession)
  107. result = await SwitchbotBaseDevice.get_devices(
  108. session, "test@example.com", "password123"
  109. )
  110. # Check that api_request was called with correct parameters
  111. mock_api_request.assert_called_once_with(
  112. session,
  113. "wonderlabs.us",
  114. "wonder/device/v3/getdevice",
  115. {"required_type": "All"},
  116. {"authorization": "test_token_123"},
  117. )
  118. # Check returned dictionary
  119. assert len(result) == 3 # Only valid devices with known models
  120. assert result["AA:BB:CC:DD:EE:FF"] == SwitchbotModel.BOT
  121. assert result["11:22:33:44:55:66"] == SwitchbotModel.CURTAIN
  122. assert result["77:88:99:AA:BB:CC"] == SwitchbotModel.LOCK
  123. # Check that cache was populated
  124. assert mock_populate_cache.call_count == 3
  125. mock_populate_cache.assert_any_call("AA:BB:CC:DD:EE:FF", SwitchbotModel.BOT)
  126. mock_populate_cache.assert_any_call("11:22:33:44:55:66", SwitchbotModel.CURTAIN)
  127. mock_populate_cache.assert_any_call("77:88:99:AA:BB:CC", SwitchbotModel.LOCK)
  128. # Check that unknown model was logged
  129. assert "Unknown model WoUnknown for device DD:EE:FF:00:11:22" in caplog.text
  130. assert "extra_field" in caplog.text # Full item should be logged
  131. @pytest.mark.asyncio
  132. async def test_get_devices_with_region(
  133. mock_auth_response: dict[str, Any],
  134. mock_device_response: dict[str, Any],
  135. caplog: pytest.LogCaptureFixture,
  136. ) -> None:
  137. """Test get_devices with different region."""
  138. mock_user_info_eu = {
  139. "botRegion": "eu",
  140. "country": "de",
  141. "email": "test@example.com",
  142. }
  143. with (
  144. patch.object(
  145. SwitchbotBaseDevice, "_get_auth_result", return_value=mock_auth_response
  146. ),
  147. patch.object(
  148. SwitchbotBaseDevice, "_async_get_user_info", return_value=mock_user_info_eu
  149. ),
  150. patch.object(
  151. SwitchbotBaseDevice, "api_request", return_value=mock_device_response
  152. ) as mock_api_request,
  153. patch("switchbot.devices.device.populate_model_to_mac_cache"),
  154. ):
  155. session = MagicMock(spec=aiohttp.ClientSession)
  156. await SwitchbotBaseDevice.get_devices(
  157. session, "test@example.com", "password123"
  158. )
  159. # Check that EU region was used
  160. mock_api_request.assert_called_once_with(
  161. session,
  162. "wonderlabs.eu",
  163. "wonder/device/v3/getdevice",
  164. {"required_type": "All"},
  165. {"authorization": "test_token_123"},
  166. )
  167. @pytest.mark.asyncio
  168. async def test_get_devices_no_region(
  169. mock_auth_response: dict[str, Any],
  170. mock_device_response: dict[str, Any],
  171. ) -> None:
  172. """Test get_devices with no region specified (defaults to us)."""
  173. mock_user_info_no_region = {
  174. "email": "test@example.com",
  175. }
  176. with (
  177. patch.object(
  178. SwitchbotBaseDevice, "_get_auth_result", return_value=mock_auth_response
  179. ),
  180. patch.object(
  181. SwitchbotBaseDevice,
  182. "_async_get_user_info",
  183. return_value=mock_user_info_no_region,
  184. ),
  185. patch.object(
  186. SwitchbotBaseDevice, "api_request", return_value=mock_device_response
  187. ) as mock_api_request,
  188. patch("switchbot.devices.device.populate_model_to_mac_cache"),
  189. ):
  190. session = MagicMock(spec=aiohttp.ClientSession)
  191. await SwitchbotBaseDevice.get_devices(
  192. session, "test@example.com", "password123"
  193. )
  194. # Check that default US region was used
  195. mock_api_request.assert_called_once_with(
  196. session,
  197. "wonderlabs.us",
  198. "wonder/device/v3/getdevice",
  199. {"required_type": "All"},
  200. {"authorization": "test_token_123"},
  201. )
  202. @pytest.mark.asyncio
  203. async def test_get_devices_empty_region(
  204. mock_auth_response: dict[str, Any],
  205. mock_device_response: dict[str, Any],
  206. ) -> None:
  207. """Test get_devices with empty region string (defaults to us)."""
  208. mock_user_info_empty_region = {
  209. "botRegion": "",
  210. "email": "test@example.com",
  211. }
  212. with (
  213. patch.object(
  214. SwitchbotBaseDevice, "_get_auth_result", return_value=mock_auth_response
  215. ),
  216. patch.object(
  217. SwitchbotBaseDevice,
  218. "_async_get_user_info",
  219. return_value=mock_user_info_empty_region,
  220. ),
  221. patch.object(
  222. SwitchbotBaseDevice, "api_request", return_value=mock_device_response
  223. ) as mock_api_request,
  224. patch("switchbot.devices.device.populate_model_to_mac_cache"),
  225. ):
  226. session = MagicMock(spec=aiohttp.ClientSession)
  227. await SwitchbotBaseDevice.get_devices(
  228. session, "test@example.com", "password123"
  229. )
  230. # Check that default US region was used
  231. mock_api_request.assert_called_once_with(
  232. session,
  233. "wonderlabs.us",
  234. "wonder/device/v3/getdevice",
  235. {"required_type": "All"},
  236. {"authorization": "test_token_123"},
  237. )
  238. @pytest.mark.asyncio
  239. async def test_fetch_cloud_devices(
  240. mock_auth_response: dict[str, Any],
  241. mock_user_info: dict[str, Any],
  242. mock_device_response: dict[str, Any],
  243. ) -> None:
  244. """Test fetch_cloud_devices wrapper function."""
  245. with (
  246. patch.object(
  247. SwitchbotBaseDevice, "_get_auth_result", return_value=mock_auth_response
  248. ),
  249. patch.object(
  250. SwitchbotBaseDevice, "_async_get_user_info", return_value=mock_user_info
  251. ),
  252. patch.object(
  253. SwitchbotBaseDevice, "api_request", return_value=mock_device_response
  254. ),
  255. patch(
  256. "switchbot.devices.device.populate_model_to_mac_cache"
  257. ) as mock_populate_cache,
  258. ):
  259. session = MagicMock(spec=aiohttp.ClientSession)
  260. result = await fetch_cloud_devices(session, "test@example.com", "password123")
  261. # Check returned dictionary
  262. assert len(result) == 3
  263. assert result["AA:BB:CC:DD:EE:FF"] == SwitchbotModel.BOT
  264. assert result["11:22:33:44:55:66"] == SwitchbotModel.CURTAIN
  265. assert result["77:88:99:AA:BB:CC"] == SwitchbotModel.LOCK
  266. # Check that cache was populated
  267. assert mock_populate_cache.call_count == 3
  268. @pytest.mark.asyncio
  269. async def test_get_devices_authentication_error() -> None:
  270. """Test get_devices with authentication error."""
  271. with patch.object(
  272. SwitchbotBaseDevice,
  273. "_get_auth_result",
  274. side_effect=Exception("Auth failed"),
  275. ):
  276. session = MagicMock(spec=aiohttp.ClientSession)
  277. with pytest.raises(SwitchbotAuthenticationError) as exc_info:
  278. await SwitchbotBaseDevice.get_devices(
  279. session, "test@example.com", "wrong_password"
  280. )
  281. assert "Authentication failed" in str(exc_info.value)
  282. @pytest.mark.asyncio
  283. async def test_get_devices_connection_error(
  284. mock_auth_response: dict[str, Any],
  285. mock_user_info: dict[str, Any],
  286. ) -> None:
  287. """Test get_devices with connection error."""
  288. with (
  289. patch.object(
  290. SwitchbotBaseDevice, "_get_auth_result", return_value=mock_auth_response
  291. ),
  292. patch.object(
  293. SwitchbotBaseDevice, "_async_get_user_info", return_value=mock_user_info
  294. ),
  295. patch.object(
  296. SwitchbotBaseDevice,
  297. "api_request",
  298. side_effect=Exception("Network error"),
  299. ),
  300. ):
  301. session = MagicMock(spec=aiohttp.ClientSession)
  302. with pytest.raises(SwitchbotAccountConnectionError) as exc_info:
  303. await SwitchbotBaseDevice.get_devices(
  304. session, "test@example.com", "password123"
  305. )
  306. assert "Failed to retrieve devices" in str(exc_info.value)
  307. @pytest.mark.asyncio
  308. async def test_populate_model_to_mac_cache() -> None:
  309. """Test the populate_model_to_mac_cache helper function."""
  310. # Clear the cache first
  311. _MODEL_TO_MAC_CACHE.clear()
  312. # Populate cache with test data
  313. populate_model_to_mac_cache("AA:BB:CC:DD:EE:FF", SwitchbotModel.BOT)
  314. populate_model_to_mac_cache("11:22:33:44:55:66", SwitchbotModel.CURTAIN)
  315. # Check cache contents
  316. assert _MODEL_TO_MAC_CACHE["AA:BB:CC:DD:EE:FF"] == SwitchbotModel.BOT
  317. assert _MODEL_TO_MAC_CACHE["11:22:33:44:55:66"] == SwitchbotModel.CURTAIN
  318. assert len(_MODEL_TO_MAC_CACHE) == 2
  319. # Clear cache after test
  320. _MODEL_TO_MAC_CACHE.clear()
  321. def test_extract_region() -> None:
  322. """Test the _extract_region helper function."""
  323. # Test with botRegion present and not empty
  324. assert _extract_region({"botRegion": "eu", "country": "de"}) == "eu"
  325. assert _extract_region({"botRegion": "us", "country": "us"}) == "us"
  326. assert _extract_region({"botRegion": "jp", "country": "jp"}) == "jp"
  327. # Test with botRegion empty string
  328. assert _extract_region({"botRegion": "", "country": "de"}) == "us"
  329. # Test with botRegion missing
  330. assert _extract_region({"country": "de"}) == "us"
  331. # Test with empty dict
  332. assert _extract_region({}) == "us"