test_rest_smart_plug.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. """Unit tests for REST smart plug service."""
  2. import json
  3. from unittest.mock import AsyncMock, MagicMock, patch
  4. import httpx
  5. import pytest
  6. from backend.app.services.rest_smart_plug import RESTSmartPlugService
  7. @pytest.fixture
  8. def service():
  9. return RESTSmartPlugService(timeout=5.0)
  10. @pytest.fixture
  11. def mock_plug():
  12. plug = MagicMock()
  13. plug.name = "Test REST Plug"
  14. plug.plug_type = "rest"
  15. plug.rest_on_url = "http://192.168.1.50:8080/api/plug/on"
  16. plug.rest_on_body = '{"state": "on"}'
  17. plug.rest_off_url = "http://192.168.1.50:8080/api/plug/off"
  18. plug.rest_off_body = '{"state": "off"}'
  19. plug.rest_method = "POST"
  20. plug.rest_headers = '{"Authorization": "Bearer test-token"}'
  21. plug.rest_status_url = "http://192.168.1.50:8080/api/plug/status"
  22. plug.rest_status_path = "state"
  23. plug.rest_status_on_value = "ON"
  24. plug.rest_power_path = "power"
  25. plug.rest_energy_path = "energy.today"
  26. return plug
  27. class TestURLValidation:
  28. def test_valid_ip_url(self, service):
  29. assert service._validate_url("http://192.168.1.50:8080/api") is True
  30. def test_hostname_url(self, service):
  31. assert service._validate_url("http://openhab.local:8080/api") is True
  32. def test_loopback_blocked(self, service):
  33. assert service._validate_url("http://127.0.0.1/api") is False
  34. def test_link_local_blocked(self, service):
  35. assert service._validate_url("http://169.254.1.1/api") is False
  36. def test_empty_hostname(self, service):
  37. assert service._validate_url("http:///api") is False
  38. class TestParseHeaders:
  39. def test_valid_json(self, service):
  40. headers = service._parse_headers('{"Authorization": "Bearer abc", "X-Custom": "val"}')
  41. assert headers == {"Authorization": "Bearer abc", "X-Custom": "val"}
  42. def test_none_headers(self, service):
  43. assert service._parse_headers(None) == {}
  44. def test_empty_string(self, service):
  45. assert service._parse_headers("") == {}
  46. def test_invalid_json(self, service):
  47. assert service._parse_headers("not json") == {}
  48. class TestExtractJsonPath:
  49. def test_simple_path(self, service):
  50. data = {"state": "ON"}
  51. assert service._extract_json_path(data, "state") == "ON"
  52. def test_nested_path(self, service):
  53. data = {"data": {"power": {"current": 42.5}}}
  54. assert service._extract_json_path(data, "data.power.current") == 42.5
  55. def test_missing_path(self, service):
  56. data = {"state": "ON"}
  57. assert service._extract_json_path(data, "missing") is None
  58. def test_empty_path(self, service):
  59. assert service._extract_json_path({"a": 1}, "") is None
  60. def test_none_path(self, service):
  61. assert service._extract_json_path({"a": 1}, None) is None
  62. class TestTurnOn:
  63. @pytest.mark.asyncio
  64. async def test_turn_on_success(self, service, mock_plug):
  65. mock_response = MagicMock()
  66. mock_response.status_code = 200
  67. with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
  68. result = await service.turn_on(mock_plug)
  69. assert result is True
  70. @pytest.mark.asyncio
  71. async def test_turn_on_failure(self, service, mock_plug):
  72. with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=None):
  73. result = await service.turn_on(mock_plug)
  74. assert result is False
  75. @pytest.mark.asyncio
  76. async def test_turn_on_no_url(self, service, mock_plug):
  77. mock_plug.rest_on_url = None
  78. result = await service.turn_on(mock_plug)
  79. assert result is False
  80. class TestTurnOff:
  81. @pytest.mark.asyncio
  82. async def test_turn_off_success(self, service, mock_plug):
  83. mock_response = MagicMock()
  84. mock_response.status_code = 200
  85. with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
  86. result = await service.turn_off(mock_plug)
  87. assert result is True
  88. @pytest.mark.asyncio
  89. async def test_turn_off_no_url(self, service, mock_plug):
  90. mock_plug.rest_off_url = None
  91. result = await service.turn_off(mock_plug)
  92. assert result is False
  93. class TestGetStatus:
  94. @pytest.mark.asyncio
  95. async def test_status_on(self, service, mock_plug):
  96. mock_response = MagicMock()
  97. mock_response.json.return_value = {"state": "ON"}
  98. with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
  99. result = await service.get_status(mock_plug)
  100. assert result["state"] == "ON"
  101. assert result["reachable"] is True
  102. @pytest.mark.asyncio
  103. async def test_status_off(self, service, mock_plug):
  104. mock_response = MagicMock()
  105. mock_response.json.return_value = {"state": "OFF"}
  106. with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
  107. result = await service.get_status(mock_plug)
  108. assert result["state"] == "OFF"
  109. assert result["reachable"] is True
  110. @pytest.mark.asyncio
  111. async def test_status_unreachable(self, service, mock_plug):
  112. with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=None):
  113. result = await service.get_status(mock_plug)
  114. assert result["state"] is None
  115. assert result["reachable"] is False
  116. @pytest.mark.asyncio
  117. async def test_status_no_url(self, service, mock_plug):
  118. mock_plug.rest_status_url = None
  119. result = await service.get_status(mock_plug)
  120. assert result["state"] is None
  121. assert result["reachable"] is True # No URL = assume reachable
  122. class TestGetEnergy:
  123. @pytest.mark.asyncio
  124. async def test_energy_with_paths(self, service, mock_plug):
  125. mock_response = MagicMock()
  126. mock_response.json.return_value = {"power": 42.5, "energy": {"today": 1.23}}
  127. with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
  128. result = await service.get_energy(mock_plug)
  129. assert result["power"] == 42.5
  130. assert result["today"] == 1.23
  131. @pytest.mark.asyncio
  132. async def test_energy_no_status_url(self, service, mock_plug):
  133. mock_plug.rest_status_url = None
  134. result = await service.get_energy(mock_plug)
  135. assert result is None
  136. @pytest.mark.asyncio
  137. async def test_energy_no_paths(self, service, mock_plug):
  138. mock_plug.rest_power_path = None
  139. mock_plug.rest_energy_path = None
  140. result = await service.get_energy(mock_plug)
  141. assert result is None
  142. class TestTestConnection:
  143. @pytest.mark.asyncio
  144. async def test_connection_success(self, service):
  145. with patch("httpx.AsyncClient") as mock_client_cls:
  146. mock_client = AsyncMock()
  147. mock_response = MagicMock()
  148. mock_response.raise_for_status = MagicMock()
  149. mock_client.request = AsyncMock(return_value=mock_response)
  150. mock_client.__aenter__ = AsyncMock(return_value=mock_client)
  151. mock_client.__aexit__ = AsyncMock(return_value=None)
  152. mock_client_cls.return_value = mock_client
  153. result = await service.test_connection("http://192.168.1.50:8080/api")
  154. assert result["success"] is True
  155. @pytest.mark.asyncio
  156. async def test_connection_timeout(self, service):
  157. with patch("httpx.AsyncClient") as mock_client_cls:
  158. mock_client = AsyncMock()
  159. mock_client.request = AsyncMock(side_effect=httpx.TimeoutException("timeout"))
  160. mock_client.__aenter__ = AsyncMock(return_value=mock_client)
  161. mock_client.__aexit__ = AsyncMock(return_value=None)
  162. mock_client_cls.return_value = mock_client
  163. result = await service.test_connection("http://192.168.1.50:8080/api")
  164. assert result["success"] is False
  165. assert "timed out" in result["error"]
  166. @pytest.mark.asyncio
  167. async def test_connection_invalid_url(self, service):
  168. result = await service.test_connection("http://127.0.0.1/api")
  169. assert result["success"] is False
  170. assert "blocked" in result["error"].lower()