"""Tests for housekeeping (home automation) endpoints.""" import pytest from fastapi.testclient import TestClient from unittest.mock import patch, AsyncMock from src.main import app @pytest.fixture def client(): """Create a test client.""" return TestClient(app) @pytest.fixture def mock_ha_client(): """Create a mock Home Assistant client.""" mock = AsyncMock() return mock @pytest.fixture def sample_states(): """Sample Home Assistant states for testing.""" return [ { "entity_id": "light.living_room", "state": "on", "attributes": { "friendly_name": "Living Room Light", "brightness": 255, "area_id": "living_room" }, "last_changed": "2025-01-01T12:00:00Z" }, { "entity_id": "light.bedroom", "state": "off", "attributes": { "friendly_name": "Bedroom Light", "area_id": "bedroom" }, "last_changed": "2025-01-01T11:00:00Z" }, { "entity_id": "switch.garage", "state": "off", "attributes": { "friendly_name": "Garage Switch" }, "last_changed": "2025-01-01T10:00:00Z" }, { "entity_id": "scene.movie_night", "state": "scening", "attributes": { "friendly_name": "Movie Night" }, "last_changed": "2025-01-01T09:00:00Z" }, { "entity_id": "script.bedtime", "state": "off", "attributes": { "friendly_name": "Bedtime Routine" }, "last_changed": "2025-01-01T08:00:00Z" }, { "entity_id": "automation.motion_lights", "state": "on", "attributes": { "friendly_name": "Motion Lights" }, "last_changed": "2025-01-01T07:00:00Z" }, { "entity_id": "sensor.temperature", "state": "22.5", "attributes": { "friendly_name": "Temperature", "unit_of_measurement": "°C" }, "last_changed": "2025-01-01T06:00:00Z" } ] class TestHousekeepingHealth: """Test /housekeeping/health endpoint.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_health_returns_200(self, mock_get_ha, client): """Health endpoint should return 200.""" mock_client = AsyncMock() mock_client.health_check.return_value = { "status": "healthy", "connected": True, "platform": "home_assistant", "version": "2024.12.0" } mock_get_ha.return_value = mock_client response = client.get("/housekeeping/health") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_health_returns_connection_status(self, mock_get_ha, client): """Health endpoint should return connection status.""" mock_client = AsyncMock() mock_client.health_check.return_value = { "status": "healthy", "connected": True, "platform": "home_assistant", "version": "2024.12.0" } mock_get_ha.return_value = mock_client response = client.get("/housekeeping/health") data = response.json() assert "status" in data assert "connected" in data assert "platform" in data assert data["platform"] == "home_assistant" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_health_returns_unhealthy_when_disconnected(self, mock_get_ha, client): """Health should report unhealthy when HA is disconnected.""" mock_client = AsyncMock() mock_client.health_check.return_value = { "status": "unhealthy", "connected": False, "platform": "home_assistant", "error": "Connection refused" } mock_get_ha.return_value = mock_client response = client.get("/housekeeping/health") data = response.json() assert data["connected"] is False assert data["status"] == "unhealthy" class TestHousekeepingDevices: """Test /housekeeping/devices endpoints.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_devices_returns_200(self, mock_get_ha, client, sample_states): """List devices should return 200.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/devices") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_devices_returns_controllable_only(self, mock_get_ha, client, sample_states): """List devices should filter out non-controllable entities.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/devices") data = response.json() # Should include lights, switches, scenes, scripts, automations # Should NOT include sensors entity_ids = [d["entity_id"] for d in data["devices"]] assert "light.living_room" in entity_ids assert "switch.garage" in entity_ids assert "sensor.temperature" not in entity_ids @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_devices_filter_by_domain(self, mock_get_ha, client, sample_states): """List devices should filter by domain parameter.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/devices?domain=light") data = response.json() # Should only return lights assert len(data["devices"]) == 2 for device in data["devices"]: assert device["domain"] == "light" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_devices_includes_attributes(self, mock_get_ha, client, sample_states): """List devices should include device attributes.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/devices") data = response.json() # Find the living room light living_room = next(d for d in data["devices"] if d["entity_id"] == "light.living_room") assert living_room["name"] == "Living Room Light" assert living_room["state"] == "on" assert "brightness" in living_room["attributes"] @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_get_device_returns_200(self, mock_get_ha, client): """Get device should return 200 for existing device.""" mock_client = AsyncMock() mock_client.get_state.return_value = { "entity_id": "light.living_room", "state": "on", "attributes": { "friendly_name": "Living Room Light", "brightness": 255 }, "last_changed": "2025-01-01T12:00:00Z" } mock_get_ha.return_value = mock_client response = client.get("/housekeeping/devices/light.living_room") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_get_device_returns_404_for_missing(self, mock_get_ha, client): """Get device should return 404 for non-existent device.""" mock_client = AsyncMock() mock_client.get_state.return_value = None mock_get_ha.return_value = mock_client response = client.get("/housekeeping/devices/light.nonexistent") assert response.status_code == 404 data = response.json() assert data["detail"]["code"] == "DEVICE_NOT_FOUND" class TestHousekeepingAreas: """Test /housekeeping/areas endpoint.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_areas_returns_200(self, mock_get_ha, client): """List areas should return 200.""" mock_client = AsyncMock() mock_client.get_areas.return_value = [ {"id": "living_room", "name": "Living Room"}, {"id": "bedroom", "name": "Bedroom"} ] mock_get_ha.return_value = mock_client response = client.get("/housekeeping/areas") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_areas_returns_area_data(self, mock_get_ha, client): """List areas should return area id and name.""" mock_client = AsyncMock() mock_client.get_areas.return_value = [ {"id": "living_room", "name": "Living Room"}, {"id": "bedroom", "name": "Bedroom"} ] mock_get_ha.return_value = mock_client response = client.get("/housekeeping/areas") data = response.json() assert "areas" in data assert len(data["areas"]) == 2 assert data["areas"][0]["id"] == "living_room" assert data["areas"][0]["name"] == "Living Room" class TestHousekeepingScenes: """Test /housekeeping/scenes endpoints.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_scenes_returns_200(self, mock_get_ha, client, sample_states): """List scenes should return 200.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/scenes") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_scenes_returns_only_scenes(self, mock_get_ha, client, sample_states): """List scenes should only return scene entities.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/scenes") data = response.json() assert "scenes" in data assert len(data["scenes"]) == 1 assert data["scenes"][0]["id"] == "scene.movie_night" assert data["scenes"][0]["name"] == "Movie Night" class TestHousekeepingScripts: """Test /housekeeping/scripts endpoints.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_scripts_returns_200(self, mock_get_ha, client, sample_states): """List scripts should return 200.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/scripts") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_scripts_returns_only_scripts(self, mock_get_ha, client, sample_states): """List scripts should only return script entities.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/scripts") data = response.json() assert "scripts" in data assert len(data["scripts"]) == 1 assert data["scripts"][0]["id"] == "script.bedtime" class TestHousekeepingAutomations: """Test /housekeeping/automations endpoints.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_automations_returns_200(self, mock_get_ha, client, sample_states): """List automations should return 200.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/automations") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_list_automations_includes_enabled_status(self, mock_get_ha, client, sample_states): """List automations should include enabled status.""" mock_client = AsyncMock() mock_client.get_states.return_value = sample_states mock_get_ha.return_value = mock_client response = client.get("/housekeeping/automations") data = response.json() assert "automations" in data assert len(data["automations"]) == 1 assert data["automations"][0]["id"] == "automation.motion_lights" assert data["automations"][0]["enabled"] is True class TestHousekeepingHistory: """Test /housekeeping/history endpoint.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_history_returns_200(self, mock_get_ha, client): """History endpoint should return 200.""" mock_client = AsyncMock() mock_client.get_history.return_value = [[ {"state": "on", "last_changed": "2025-01-01T12:00:00Z", "attributes": {}}, {"state": "off", "last_changed": "2025-01-01T11:00:00Z", "attributes": {}} ]] mock_get_ha.return_value = mock_client response = client.get("/housekeeping/history?entity_id=light.living_room") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_history_returns_entries(self, mock_get_ha, client): """History endpoint should return history entries.""" mock_client = AsyncMock() mock_client.get_history.return_value = [[ {"state": "on", "last_changed": "2025-01-01T12:00:00Z", "attributes": {"brightness": 255}}, {"state": "off", "last_changed": "2025-01-01T11:00:00Z", "attributes": {}} ]] mock_get_ha.return_value = mock_client response = client.get("/housekeeping/history?entity_id=light.living_room&hours=24") data = response.json() assert "entity_id" in data assert "history" in data assert data["entity_id"] == "light.living_room" assert len(data["history"]) == 2 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_history_requires_entity_id(self, mock_get_ha, client): """History endpoint should require entity_id parameter.""" response = client.get("/housekeeping/history") assert response.status_code == 422 # Validation error @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_history_validates_hours_range(self, mock_get_ha, client): """History endpoint should validate hours range (1-168).""" mock_client = AsyncMock() mock_get_ha.return_value = mock_client # Too high response = client.get("/housekeeping/history?entity_id=light.test&hours=200") assert response.status_code == 422 # Too low response = client.get("/housekeeping/history?entity_id=light.test&hours=0") assert response.status_code == 422 class TestHousekeepingDeviceControl: """Test /housekeeping/devices/{entity_id}/control endpoint.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_control_device_turn_on(self, mock_get_ha, client): """Control should turn on device.""" mock_client = AsyncMock() mock_client.get_state.return_value = { "entity_id": "light.living_room", "state": "on", "attributes": {"brightness": 255} } mock_client.turn_on.return_value = [{"entity_id": "light.living_room"}] mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/devices/light.living_room/control", json={"action": "turn_on"} ) assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_control_device_turn_off(self, mock_get_ha, client): """Control should turn off device.""" mock_client = AsyncMock() mock_client.get_state.return_value = { "entity_id": "light.living_room", "state": "off", "attributes": {} } mock_client.turn_off.return_value = [{"entity_id": "light.living_room"}] mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/devices/light.living_room/control", json={"action": "turn_off"} ) assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_control_device_toggle(self, mock_get_ha, client): """Control should toggle device.""" mock_client = AsyncMock() mock_client.get_state.return_value = { "entity_id": "light.living_room", "state": "on", "attributes": {} } mock_client.toggle.return_value = [{"entity_id": "light.living_room"}] mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/devices/light.living_room/control", json={"action": "toggle"} ) assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_control_device_with_brightness(self, mock_get_ha, client): """Control should set brightness.""" mock_client = AsyncMock() mock_client.get_state.return_value = { "entity_id": "light.living_room", "state": "on", "attributes": {"brightness": 128} } mock_client.turn_on.return_value = [{"entity_id": "light.living_room"}] mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/devices/light.living_room/control", json={"action": "turn_on", "brightness": 128} ) assert response.status_code == 200 mock_client.turn_on.assert_called_once() @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_control_device_returns_404_for_missing(self, mock_get_ha, client): """Control should return 404 for non-existent device.""" mock_client = AsyncMock() mock_client.get_state.return_value = None mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/devices/light.nonexistent/control", json={"action": "turn_on"} ) assert response.status_code == 404 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_control_device_returns_error_response(self, mock_get_ha, client): """Control should return proper error response.""" mock_client = AsyncMock() mock_client.get_state.return_value = { "entity_id": "light.living_room", "state": "on", "attributes": {} } mock_client.turn_on.side_effect = Exception("Service unavailable") mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/devices/light.living_room/control", json={"action": "turn_on"} ) assert response.status_code == 500 class TestHousekeepingSceneActivation: """Test /housekeeping/scenes/{scene_id}/activate endpoint.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_activate_scene_returns_200(self, mock_get_ha, client): """Activate scene should return 200.""" mock_client = AsyncMock() mock_client.activate_scene.return_value = [{"entity_id": "scene.movie_night"}] mock_get_ha.return_value = mock_client response = client.post("/housekeeping/scenes/scene.movie_night/activate") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_activate_scene_returns_success_response(self, mock_get_ha, client): """Activate scene should return success response.""" mock_client = AsyncMock() mock_client.activate_scene.return_value = [{"entity_id": "scene.movie_night"}] mock_get_ha.return_value = mock_client response = client.post("/housekeeping/scenes/scene.movie_night/activate") data = response.json() assert data["success"] is True assert data["scene_id"] == "scene.movie_night" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_activate_scene_handles_error(self, mock_get_ha, client): """Activate scene should handle errors.""" mock_client = AsyncMock() mock_client.activate_scene.side_effect = Exception("Service error") mock_get_ha.return_value = mock_client response = client.post("/housekeeping/scenes/scene.movie_night/activate") assert response.status_code == 500 class TestHousekeepingScriptRun: """Test /housekeeping/scripts/{script_id}/run endpoint.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_run_script_returns_200(self, mock_get_ha, client): """Run script should return 200.""" mock_client = AsyncMock() mock_client.run_script.return_value = [{"entity_id": "script.bedtime"}] mock_get_ha.return_value = mock_client response = client.post("/housekeeping/scripts/script.bedtime/run") assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_run_script_returns_success_response(self, mock_get_ha, client): """Run script should return success response.""" mock_client = AsyncMock() mock_client.run_script.return_value = [{"entity_id": "script.bedtime"}] mock_get_ha.return_value = mock_client response = client.post("/housekeeping/scripts/script.bedtime/run") data = response.json() assert data["success"] is True assert data["script_id"] == "script.bedtime" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_run_script_handles_error(self, mock_get_ha, client): """Run script should handle errors.""" mock_client = AsyncMock() mock_client.run_script.side_effect = Exception("Script error") mock_get_ha.return_value = mock_client response = client.post("/housekeeping/scripts/script.bedtime/run") assert response.status_code == 500 class TestHousekeepingAutomationToggle: """Test /housekeeping/automations/{automation_id}/toggle endpoint.""" @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_toggle_automation_enable(self, mock_get_ha, client): """Toggle automation should enable when requested.""" mock_client = AsyncMock() mock_client.enable_automation.return_value = [{"entity_id": "automation.motion_lights"}] mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/automations/automation.motion_lights/toggle", json={"enabled": True} ) assert response.status_code == 200 @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_toggle_automation_disable(self, mock_get_ha, client): """Toggle automation should disable when requested.""" mock_client = AsyncMock() mock_client.disable_automation.return_value = [{"entity_id": "automation.motion_lights"}] mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/automations/automation.motion_lights/toggle", json={"enabled": False} ) assert response.status_code == 200 mock_client.disable_automation.assert_called_once() @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_toggle_automation_returns_new_state(self, mock_get_ha, client): """Toggle automation should return new enabled state.""" mock_client = AsyncMock() mock_client.enable_automation.return_value = [{"entity_id": "automation.motion_lights"}] mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/automations/automation.motion_lights/toggle", json={"enabled": True} ) data = response.json() assert data["success"] is True assert data["automation_id"] == "automation.motion_lights" assert data["enabled"] is True @patch("src.controllers.housekeeping_controller.get_homeassistant_client") def test_toggle_automation_handles_error(self, mock_get_ha, client): """Toggle automation should handle errors.""" mock_client = AsyncMock() mock_client.enable_automation.side_effect = Exception("Automation error") mock_get_ha.return_value = mock_client response = client.post( "/housekeeping/automations/automation.motion_lights/toggle", json={"enabled": True} ) assert response.status_code == 500