| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- """Unit tests for REST smart plug service."""
- import json
- from unittest.mock import AsyncMock, MagicMock, patch
- import httpx
- import pytest
- from backend.app.services.rest_smart_plug import RESTSmartPlugService
- @pytest.fixture
- def service():
- return RESTSmartPlugService(timeout=5.0)
- @pytest.fixture
- def mock_plug():
- plug = MagicMock()
- plug.name = "Test REST Plug"
- plug.plug_type = "rest"
- plug.rest_on_url = "http://192.168.1.50:8080/api/plug/on"
- plug.rest_on_body = '{"state": "on"}'
- plug.rest_off_url = "http://192.168.1.50:8080/api/plug/off"
- plug.rest_off_body = '{"state": "off"}'
- plug.rest_method = "POST"
- plug.rest_headers = '{"Authorization": "Bearer test-token"}'
- plug.rest_status_url = "http://192.168.1.50:8080/api/plug/status"
- plug.rest_status_path = "state"
- plug.rest_status_on_value = "ON"
- plug.rest_power_path = "power"
- plug.rest_energy_path = "energy.today"
- return plug
- class TestURLValidation:
- def test_valid_ip_url(self, service):
- assert service._validate_url("http://192.168.1.50:8080/api") is True
- def test_hostname_url(self, service):
- assert service._validate_url("http://openhab.local:8080/api") is True
- def test_loopback_blocked(self, service):
- assert service._validate_url("http://127.0.0.1/api") is False
- def test_link_local_blocked(self, service):
- assert service._validate_url("http://169.254.1.1/api") is False
- def test_empty_hostname(self, service):
- assert service._validate_url("http:///api") is False
- class TestParseHeaders:
- def test_valid_json(self, service):
- headers = service._parse_headers('{"Authorization": "Bearer abc", "X-Custom": "val"}')
- assert headers == {"Authorization": "Bearer abc", "X-Custom": "val"}
- def test_none_headers(self, service):
- assert service._parse_headers(None) == {}
- def test_empty_string(self, service):
- assert service._parse_headers("") == {}
- def test_invalid_json(self, service):
- assert service._parse_headers("not json") == {}
- class TestExtractJsonPath:
- def test_simple_path(self, service):
- data = {"state": "ON"}
- assert service._extract_json_path(data, "state") == "ON"
- def test_nested_path(self, service):
- data = {"data": {"power": {"current": 42.5}}}
- assert service._extract_json_path(data, "data.power.current") == 42.5
- def test_missing_path(self, service):
- data = {"state": "ON"}
- assert service._extract_json_path(data, "missing") is None
- def test_empty_path(self, service):
- assert service._extract_json_path({"a": 1}, "") is None
- def test_none_path(self, service):
- assert service._extract_json_path({"a": 1}, None) is None
- class TestTurnOn:
- @pytest.mark.asyncio
- async def test_turn_on_success(self, service, mock_plug):
- mock_response = MagicMock()
- mock_response.status_code = 200
- with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
- result = await service.turn_on(mock_plug)
- assert result is True
- @pytest.mark.asyncio
- async def test_turn_on_failure(self, service, mock_plug):
- with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=None):
- result = await service.turn_on(mock_plug)
- assert result is False
- @pytest.mark.asyncio
- async def test_turn_on_no_url(self, service, mock_plug):
- mock_plug.rest_on_url = None
- result = await service.turn_on(mock_plug)
- assert result is False
- class TestTurnOff:
- @pytest.mark.asyncio
- async def test_turn_off_success(self, service, mock_plug):
- mock_response = MagicMock()
- mock_response.status_code = 200
- with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
- result = await service.turn_off(mock_plug)
- assert result is True
- @pytest.mark.asyncio
- async def test_turn_off_no_url(self, service, mock_plug):
- mock_plug.rest_off_url = None
- result = await service.turn_off(mock_plug)
- assert result is False
- class TestGetStatus:
- @pytest.mark.asyncio
- async def test_status_on(self, service, mock_plug):
- mock_response = MagicMock()
- mock_response.json.return_value = {"state": "ON"}
- with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
- result = await service.get_status(mock_plug)
- assert result["state"] == "ON"
- assert result["reachable"] is True
- @pytest.mark.asyncio
- async def test_status_off(self, service, mock_plug):
- mock_response = MagicMock()
- mock_response.json.return_value = {"state": "OFF"}
- with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
- result = await service.get_status(mock_plug)
- assert result["state"] == "OFF"
- assert result["reachable"] is True
- @pytest.mark.asyncio
- async def test_status_unreachable(self, service, mock_plug):
- with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=None):
- result = await service.get_status(mock_plug)
- assert result["state"] is None
- assert result["reachable"] is False
- @pytest.mark.asyncio
- async def test_status_no_url(self, service, mock_plug):
- mock_plug.rest_status_url = None
- result = await service.get_status(mock_plug)
- assert result["state"] is None
- assert result["reachable"] is True # No URL = assume reachable
- class TestGetEnergy:
- @pytest.mark.asyncio
- async def test_energy_with_paths(self, service, mock_plug):
- mock_response = MagicMock()
- mock_response.json.return_value = {"power": 42.5, "energy": {"today": 1.23}}
- with patch.object(service, "_send_request", new_callable=AsyncMock, return_value=mock_response):
- result = await service.get_energy(mock_plug)
- assert result["power"] == 42.5
- assert result["today"] == 1.23
- @pytest.mark.asyncio
- async def test_energy_no_status_url(self, service, mock_plug):
- mock_plug.rest_status_url = None
- result = await service.get_energy(mock_plug)
- assert result is None
- @pytest.mark.asyncio
- async def test_energy_no_paths(self, service, mock_plug):
- mock_plug.rest_power_path = None
- mock_plug.rest_energy_path = None
- result = await service.get_energy(mock_plug)
- assert result is None
- class TestTestConnection:
- @pytest.mark.asyncio
- async def test_connection_success(self, service):
- with patch("httpx.AsyncClient") as mock_client_cls:
- mock_client = AsyncMock()
- mock_response = MagicMock()
- mock_response.raise_for_status = MagicMock()
- mock_client.request = AsyncMock(return_value=mock_response)
- mock_client.__aenter__ = AsyncMock(return_value=mock_client)
- mock_client.__aexit__ = AsyncMock(return_value=None)
- mock_client_cls.return_value = mock_client
- result = await service.test_connection("http://192.168.1.50:8080/api")
- assert result["success"] is True
- @pytest.mark.asyncio
- async def test_connection_timeout(self, service):
- with patch("httpx.AsyncClient") as mock_client_cls:
- mock_client = AsyncMock()
- mock_client.request = AsyncMock(side_effect=httpx.TimeoutException("timeout"))
- mock_client.__aenter__ = AsyncMock(return_value=mock_client)
- mock_client.__aexit__ = AsyncMock(return_value=None)
- mock_client_cls.return_value = mock_client
- result = await service.test_connection("http://192.168.1.50:8080/api")
- assert result["success"] is False
- assert "timed out" in result["error"]
- @pytest.mark.asyncio
- async def test_connection_invalid_url(self, service):
- result = await service.test_connection("http://127.0.0.1/api")
- assert result["success"] is False
- assert "blocked" in result["error"].lower()
|