""" Tests for the Core-API HTTP client. """ from unittest.mock import AsyncMock, MagicMock import httpx import pytest from src.agents.housekeeper.client import ( Area, Automation, ControlResult, CoreAPIClient, Device, DeviceState, HistoryEntry, Scene, Script, ) @pytest.fixture def mock_httpx_client(): """Create a mock httpx client.""" return AsyncMock(spec=httpx.AsyncClient) @pytest.fixture def client_with_mock(mock_httpx_client): """Create a CoreAPIClient with mocked httpx client.""" client = CoreAPIClient( base_url="http://test:8090", api_key="test-key", ) client._client = mock_httpx_client return client @pytest.mark.unit class TestCoreAPIClientInit: """Tests for client initialization.""" def test_default_initialization(self): """Test client initializes with defaults from config.""" client = CoreAPIClient() assert client.base_url is not None assert client.timeout == 30 assert client._client is None def test_custom_initialization(self): """Test client with custom parameters.""" client = CoreAPIClient( base_url="http://custom:9000", api_key="my-api-key", timeout=60, ) assert client.base_url == "http://custom:9000" assert client.api_key == "my-api-key" assert client.timeout == 60 def test_ensure_client_not_initialized(self): """Test _ensure_client raises when not in context.""" client = CoreAPIClient() with pytest.raises(RuntimeError) as exc_info: client._ensure_client() assert "not initialized" in str(exc_info.value) @pytest.mark.unit class TestContextManager: """Tests for async context manager.""" @pytest.mark.asyncio async def test_context_manager_creates_client(self): """Test context manager creates httpx client.""" async with CoreAPIClient( base_url="http://test:8090", api_key="test-key", ) as client: assert client._client is not None @pytest.mark.asyncio async def test_context_manager_closes_client(self): """Test context manager closes client on exit.""" client = CoreAPIClient(base_url="http://test:8090") async with client: assert client._client is not None # After exit, client should be None assert client._client is None @pytest.mark.unit class TestDeviceDiscovery: """Tests for device discovery methods.""" @pytest.mark.asyncio async def test_list_devices(self, client_with_mock, mock_httpx_client): """Test listing devices.""" mock_response = MagicMock() mock_response.json.return_value = { "devices": [ { "entity_id": "light.living_room", "name": "Living Room Light", "state": "on", "domain": "light", "area": "living_room", "attributes": {"brightness": 255}, }, { "entity_id": "switch.coffee_maker", "name": "Coffee Maker", "state": "off", "domain": "switch", "area": "kitchen", }, ] } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response devices = await client_with_mock.list_devices() assert len(devices) == 2 assert isinstance(devices[0], Device) assert devices[0].entity_id == "light.living_room" assert devices[0].state == "on" assert devices[0].domain == "light" @pytest.mark.asyncio async def test_list_areas(self, client_with_mock, mock_httpx_client): """Test listing areas.""" mock_response = MagicMock() mock_response.json.return_value = { "areas": [ { "area_id": "living_room", "name": "Living Room", "device_count": 5, }, { "area_id": "bedroom", "name": "Bedroom", "device_count": 3, }, ] } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response areas = await client_with_mock.list_areas() assert len(areas) == 2 assert isinstance(areas[0], Area) assert areas[0].area_id == "living_room" assert areas[0].name == "Living Room" assert areas[0].device_count == 5 @pytest.mark.asyncio async def test_list_devices_with_filter(self, client_with_mock, mock_httpx_client): """Test listing devices with domain filter.""" mock_response = MagicMock() mock_response.json.return_value = { "devices": [ { "entity_id": "light.bedroom", "name": "Bedroom Light", "state": "off", "domain": "light", } ] } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response devices = await client_with_mock.list_devices(domain="light") assert len(devices) == 1 mock_httpx_client.get.assert_called_once() @pytest.mark.asyncio async def test_get_device_state(self, client_with_mock, mock_httpx_client): """Test getting device state.""" mock_response = MagicMock() mock_response.json.return_value = { "entity_id": "light.living_room", "state": "on", "attributes": { "brightness": 200, "color_temp": 370, }, "last_changed": "2024-01-15T10:30:00Z", } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response state = await client_with_mock.get_device_state("light.living_room") assert isinstance(state, DeviceState) assert state.entity_id == "light.living_room" assert state.state == "on" assert state.attributes["brightness"] == 200 @pytest.mark.unit class TestDeviceControl: """Tests for device control methods.""" @pytest.mark.asyncio async def test_turn_on(self, client_with_mock, mock_httpx_client): """Test turning on a device.""" mock_response = MagicMock() mock_response.json.return_value = { "success": True, "message": "Turned on", } mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.turn_on("light.living_room") assert isinstance(result, ControlResult) assert result.success is True assert result.entity_id == "light.living_room" assert result.action == "turn_on" @pytest.mark.asyncio async def test_turn_on_with_brightness(self, client_with_mock, mock_httpx_client): """Test turning on with brightness.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True} mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.turn_on( "light.bedroom", brightness=128, ) assert result.success is True # Check that brightness was in the payload call_kwargs = mock_httpx_client.post.call_args[1] assert call_kwargs["json"]["brightness"] == 128 @pytest.mark.asyncio async def test_turn_off(self, client_with_mock, mock_httpx_client): """Test turning off a device.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True} mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.turn_off("switch.coffee_maker") assert result.success is True assert result.action == "turn_off" @pytest.mark.asyncio async def test_toggle(self, client_with_mock, mock_httpx_client): """Test toggling a device.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True} mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.toggle("light.hallway") assert result.success is True assert result.action == "toggle" @pytest.mark.unit class TestScenes: """Tests for scene methods.""" @pytest.mark.asyncio async def test_list_scenes(self, client_with_mock, mock_httpx_client): """Test listing scenes.""" mock_response = MagicMock() mock_response.json.return_value = { "scenes": [ { "entity_id": "scene.movie_night", "name": "movie_night", "friendly_name": "Movie Night", }, { "entity_id": "scene.good_morning", "name": "good_morning", "friendly_name": "Good Morning", }, ] } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response scenes = await client_with_mock.list_scenes() assert len(scenes) == 2 assert isinstance(scenes[0], Scene) assert scenes[0].entity_id == "scene.movie_night" @pytest.mark.asyncio async def test_activate_scene(self, client_with_mock, mock_httpx_client): """Test activating a scene.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True} mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.activate_scene("scene.movie_night") assert result.success is True assert result.action == "activate" @pytest.mark.unit class TestScripts: """Tests for script methods.""" @pytest.mark.asyncio async def test_list_scripts(self, client_with_mock, mock_httpx_client): """Test listing scripts.""" mock_response = MagicMock() mock_response.json.return_value = { "scripts": [ { "entity_id": "script.good_morning", "name": "Good Morning Routine", "description": "Morning automation", }, ] } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response scripts = await client_with_mock.list_scripts() assert len(scripts) == 1 assert isinstance(scripts[0], Script) assert scripts[0].name == "Good Morning Routine" @pytest.mark.asyncio async def test_run_script(self, client_with_mock, mock_httpx_client): """Test running a script.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True} mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.run_script("script.good_morning") assert result.success is True assert result.action == "run" @pytest.mark.unit class TestAutomations: """Tests for automation methods.""" @pytest.mark.asyncio async def test_list_automations(self, client_with_mock, mock_httpx_client): """Test listing automations.""" mock_response = MagicMock() mock_response.json.return_value = { "automations": [ { "entity_id": "automation.morning_lights", "name": "Morning Lights", "state": "on", }, { "entity_id": "automation.vacation_mode", "name": "Vacation Mode", "state": "off", }, ] } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response automations = await client_with_mock.list_automations() assert len(automations) == 2 assert isinstance(automations[0], Automation) assert automations[0].state == "on" @pytest.mark.asyncio async def test_toggle_automation_enable(self, client_with_mock, mock_httpx_client): """Test enabling an automation.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True} mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.toggle_automation( "automation.vacation_mode", enable=True, ) assert result.success is True assert result.action == "enable" @pytest.mark.asyncio async def test_toggle_automation_disable(self, client_with_mock, mock_httpx_client): """Test disabling an automation.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True} mock_response.raise_for_status = MagicMock() mock_httpx_client.post.return_value = mock_response result = await client_with_mock.toggle_automation( "automation.morning_lights", enable=False, ) assert result.action == "disable" @pytest.mark.unit class TestHistory: """Tests for history methods.""" @pytest.mark.asyncio async def test_get_history(self, client_with_mock, mock_httpx_client): """Test getting device history.""" mock_response = MagicMock() mock_response.json.return_value = { "history": [ { "state": "on", "timestamp": "2024-01-15T08:00:00Z", "attributes": {"brightness": 255}, }, { "state": "off", "timestamp": "2024-01-15T10:30:00Z", "attributes": {}, }, ] } mock_response.raise_for_status = MagicMock() mock_httpx_client.get.return_value = mock_response history = await client_with_mock.get_history("light.living_room") assert len(history) == 2 assert isinstance(history[0], HistoryEntry) assert history[0].state == "on" assert history[1].state == "off" @pytest.mark.unit class TestHealthCheck: """Tests for health check.""" @pytest.mark.asyncio async def test_health_check_healthy(self, client_with_mock, mock_httpx_client): """Test health check returns true when healthy.""" mock_response = MagicMock() mock_response.status_code = 200 mock_httpx_client.get.return_value = mock_response result = await client_with_mock.health_check() assert result is True @pytest.mark.asyncio async def test_health_check_unhealthy(self, client_with_mock, mock_httpx_client): """Test health check returns false on error.""" mock_httpx_client.get.side_effect = httpx.ConnectError("Connection refused") result = await client_with_mock.health_check() assert result is False @pytest.mark.unit class TestResponseModels: """Tests for response model validation.""" def test_device_model(self): """Test Device model.""" device = Device( entity_id="light.test", name="Test Light", state="on", domain="light", area="bedroom", attributes={"brightness": 255}, ) assert device.entity_id == "light.test" assert device.state == "on" assert device.attributes["brightness"] == 255 def test_device_model_optional_fields(self): """Test Device with minimal fields.""" device = Device( entity_id="switch.test", name="Test Switch", state="off", domain="switch", ) assert device.area is None assert device.attributes == {} def test_area_model(self): """Test Area model.""" area = Area( area_id="living_room", name="Living Room", device_count=5, ) assert area.area_id == "living_room" assert area.name == "Living Room" assert area.device_count == 5 def test_area_model_defaults(self): """Test Area with default device_count.""" area = Area( area_id="bedroom", name="Bedroom", ) assert area.device_count == 0 def test_control_result_model(self): """Test ControlResult model.""" result = ControlResult( success=True, entity_id="light.test", action="turn_on", message="Success", ) assert result.success is True assert result.action == "turn_on" def test_history_entry_model(self): """Test HistoryEntry model.""" entry = HistoryEntry( state="on", timestamp="2024-01-15T10:00:00Z", attributes={"brightness": 200}, ) assert entry.state == "on" assert entry.attributes["brightness"] == 200