""" Tests for health checkers module """ from unittest.mock import MagicMock, patch import pytest from httpx import HTTPError, TimeoutException from services.health_checkers.api_checker import APIHealthChecker from services.health_checkers.base import HealthCheckResult from services.health_checkers.custom_checker import CustomHealthChecker from services.health_checkers.registry import HealthCheckerFactory, HealthCheckerRegistry from services.health_checkers.sensor_checker import SensorHealthChecker class TestHealthCheckResult: """Test HealthCheckResult class""" def test_health_check_result_creation(self): """Test creating a HealthCheckResult""" result = HealthCheckResult("healthy", 1.5, "No error", {"key": "value"}, "2h 30m") assert result.status == "healthy" assert result.response_time == 1.5 assert result.error == "No error" assert result.metadata == {"key": "value"} assert result.uptime == "2h 30m" def test_health_check_result_to_dict(self): """Test converting HealthCheckResult to dictionary""" result = HealthCheckResult("healthy", 1.5, "No error", {"key": "value"}, "2h 30m") result_dict = result.to_dict() expected = {"status": "healthy", "response_time": 1.5, "error": "No error", "uptime": "2h 30m", "metadata": {"key": "value"}} assert result_dict == expected def test_health_check_result_minimal(self): """Test creating minimal HealthCheckResult""" result = HealthCheckResult("healthy") assert result.status == "healthy" assert result.response_time is None assert result.error is None assert result.metadata == {} assert result.uptime is None class TestAPIHealthChecker: """Test APIHealthChecker class""" @pytest.fixture def api_checker(self): """Create APIHealthChecker instance""" return APIHealthChecker(timeout=5.0) @pytest.mark.asyncio async def test_check_health_success(self, api_checker): """Test successful health check""" config = {"enabled": True, "url": "http://test.local:8080", "health_endpoint": "/health"} # Mock successful response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "ok"} mock_response.content = b'{"status": "ok"}' with patch.object(api_checker.client, "get", return_value=mock_response): result = await api_checker.check_health("test_service", config) assert isinstance(result, HealthCheckResult) assert result.status == "healthy" assert result.response_time is not None assert result.error is None @pytest.mark.asyncio async def test_check_health_unauthorized(self, api_checker): """Test unauthorized response""" config = {"enabled": True, "url": "http://test.local:8080", "health_endpoint": "/health"} mock_response = MagicMock() mock_response.status_code = 401 with patch.object(api_checker.client, "get", return_value=mock_response): result = await api_checker.check_health("test_service", config) assert result.status == "unauthorized" assert result.error == "Authentication required" @pytest.mark.asyncio async def test_check_health_not_found(self, api_checker): """Test not found response""" config = {"enabled": True, "url": "http://test.local:8080", "health_endpoint": "/health"} mock_response = MagicMock() mock_response.status_code = 404 with patch.object(api_checker.client, "get", return_value=mock_response): result = await api_checker.check_health("test_service", config) assert result.status == "unhealthy" assert "404" in result.error @pytest.mark.asyncio async def test_check_health_timeout(self, api_checker): """Test timeout error""" config = {"enabled": True, "url": "http://test.local:8080", "health_endpoint": "/health"} with patch.object(api_checker.client, "get", side_effect=TimeoutException("Timeout")): result = await api_checker.check_health("test_service", config) assert result.status == "timeout" assert "timed out" in result.error.lower() @pytest.mark.asyncio async def test_check_health_http_error(self, api_checker): """Test HTTP error""" config = {"enabled": True, "url": "http://test.local:8080", "health_endpoint": "/health"} with patch.object(api_checker.client, "get", side_effect=HTTPError("HTTP Error")): result = await api_checker.check_health("test_service", config) assert result.status == "error" assert "http error" in result.error.lower() @pytest.mark.asyncio async def test_check_health_disabled(self, api_checker): """Test disabled service""" config = {"enabled": False, "url": "http://test.local:8080", "health_endpoint": "/health"} result = await api_checker.check_health("test_service", config) assert result.status == "disabled" @pytest.mark.asyncio async def test_check_health_no_url(self, api_checker): """Test missing URL""" config = {"enabled": True, "health_endpoint": "/health"} result = await api_checker.check_health("test_service", config) assert result.status == "error" assert "url" in result.error.lower() @pytest.mark.asyncio async def test_extract_uptime_from_response(self, api_checker): """Test uptime extraction from response""" mock_response = MagicMock() mock_response.json.return_value = {"uptime": "2h 30m"} uptime = api_checker._extract_uptime_from_response(mock_response, "test_service") assert uptime == "2h 30m" @pytest.mark.asyncio async def test_extract_uptime_no_uptime(self, api_checker): """Test uptime extraction when no uptime in response""" mock_response = MagicMock() mock_response.json.return_value = {"status": "ok"} uptime = api_checker._extract_uptime_from_response(mock_response, "test_service") assert uptime is None @pytest.mark.asyncio async def test_close(self, api_checker): """Test closing the checker""" with patch.object(api_checker.client, "aclose") as mock_close: await api_checker.close() mock_close.assert_called_once() class TestSensorHealthChecker: """Test SensorHealthChecker class""" @pytest.fixture def sensor_checker(self): """Create SensorHealthChecker instance""" return SensorHealthChecker(timeout=5.0) @pytest.mark.asyncio async def test_check_health_success(self, sensor_checker): """Test successful sensor health check""" config = {"enabled": True, "url": "http://test.local:8123", "sensor_entity": "sensor.uptime_test", "token": "test_token"} # Mock successful response mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "entity_id": "sensor.uptime_test", "state": "2025-09-18T10:00:00+00:00", "attributes": {"device_class": "timestamp"}, } with patch.object(sensor_checker.client, "get", return_value=mock_response): result = await sensor_checker.check_health("test_service", config) assert isinstance(result, HealthCheckResult) assert result.status in ["healthy", "unhealthy"] # Depends on timestamp assert result.response_time is not None @pytest.mark.asyncio async def test_check_health_no_sensor_entity(self, sensor_checker): """Test missing sensor entity""" config = {"enabled": True, "url": "http://test.local:8123"} result = await sensor_checker.check_health("test_service", config) assert result.status == "error" assert "sensor entity" in result.error.lower() def test_parse_home_assistant_sensor_uptime(self, sensor_checker): """Test parsing HA uptime sensor""" state = "2025-09-18T10:00:00+00:00" entity_id = "sensor.uptime_test" attributes = {"device_class": "timestamp"} result = sensor_checker._parse_home_assistant_sensor(state, entity_id, attributes) assert result in ["healthy", "unhealthy"] def test_parse_home_assistant_sensor_system(self, sensor_checker): """Test parsing HA system sensor""" state = "ok" entity_id = "sensor.system_health" attributes = {} result = sensor_checker._parse_home_assistant_sensor(state, entity_id, attributes) assert result == "healthy" def test_parse_timestamp_sensor_recent(self, sensor_checker): """Test parsing recent timestamp sensor""" from datetime import datetime, timedelta, timezone recent_time = datetime.now(timezone.utc) - timedelta(hours=1) state = recent_time.isoformat() result = sensor_checker._parse_timestamp_sensor(state) assert result == "healthy" def test_parse_timestamp_sensor_old(self, sensor_checker): """Test parsing old timestamp sensor""" from datetime import datetime, timedelta, timezone old_time = datetime.now(timezone.utc) - timedelta(days=2) state = old_time.isoformat() result = sensor_checker._parse_timestamp_sensor(state) assert result == "unhealthy" def test_parse_numeric_uptime_sensor(self, sensor_checker): """Test parsing numeric uptime sensor""" result = sensor_checker._parse_numeric_uptime_sensor("3600") assert result == "healthy" result = sensor_checker._parse_numeric_uptime_sensor("0") assert result == "unhealthy" def test_parse_system_sensor(self, sensor_checker): """Test parsing system sensor""" result = sensor_checker._parse_system_sensor("ok") assert result == "healthy" result = sensor_checker._parse_system_sensor("error") assert result == "unhealthy" def test_parse_generic_sensor(self, sensor_checker): """Test parsing generic sensor""" result = sensor_checker._parse_generic_sensor("online") assert result == "healthy" result = sensor_checker._parse_generic_sensor("unavailable") assert result == "unhealthy" def test_extract_uptime_info(self, sensor_checker): """Test extracting uptime info""" sensor_data = {"entity_id": "sensor.uptime_test", "state": "2025-09-18T10:00:00+00:00", "attributes": {"device_class": "timestamp"}} uptime = sensor_checker._extract_uptime_info(sensor_data, "test_service") assert uptime == "2025-09-18T10:00:00+00:00" def test_extract_uptime_info_numeric(self, sensor_checker): """Test extracting numeric uptime info""" sensor_data = {"entity_id": "sensor.uptime_test", "state": "3600", "attributes": {"device_class": "duration"}} uptime = sensor_checker._extract_uptime_info(sensor_data, "test_service") assert uptime == "3600" class TestCustomHealthChecker: """Test CustomHealthChecker class""" @pytest.fixture def custom_checker(self): """Create CustomHealthChecker instance""" return CustomHealthChecker(timeout=5.0) @pytest.mark.asyncio async def test_check_health_success(self, custom_checker): """Test successful custom health check""" config = { "enabled": True, "url": "http://test.local:8080", "health_checks": [{"type": "api", "endpoint": "/health"}, {"type": "api", "endpoint": "/status"}], } # Mock successful responses mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "ok"} with patch.object(custom_checker.client, "get", return_value=mock_response): result = await custom_checker.check_health("test_service", config) assert isinstance(result, HealthCheckResult) # Custom checker may return error if health_checks format is not recognized assert result.status in ["healthy", "unhealthy", "error"] @pytest.mark.asyncio async def test_check_health_disabled(self, custom_checker): """Test disabled service""" config = {"enabled": False, "url": "http://test.local:8080"} result = await custom_checker.check_health("test_service", config) assert result.status == "disabled" def test_determine_overall_status_all_healthy(self, custom_checker): """Test determining overall status when all checks are healthy""" results = [HealthCheckResult("healthy"), HealthCheckResult("healthy"), HealthCheckResult("healthy")] status = custom_checker._determine_overall_status(results) assert status == "healthy" def test_determine_overall_status_mixed(self, custom_checker): """Test determining overall status with mixed results""" results = [HealthCheckResult("healthy"), HealthCheckResult("unhealthy"), HealthCheckResult("healthy")] status = custom_checker._determine_overall_status(results) assert status == "unhealthy" def test_determine_overall_status_empty(self, custom_checker): """Test determining overall status with empty results""" results = [] status = custom_checker._determine_overall_status(results) assert status == "error" class TestHealthCheckerRegistry: """Test HealthCheckerRegistry class""" def test_registry_creation(self): """Test creating registry""" registry = HealthCheckerRegistry() assert registry is not None def test_register_checker(self): """Test registering a checker""" registry = HealthCheckerRegistry() registry.register("test", APIHealthChecker) checker_class = registry.get_checker("test") assert checker_class == APIHealthChecker def test_get_checker_unknown(self): """Test getting unknown checker""" registry = HealthCheckerRegistry() with pytest.raises(ValueError): registry.get_checker("unknown") def test_get_checker_types(self): """Test getting available checker types""" registry = HealthCheckerRegistry() types = list(registry._checkers.keys()) assert "api" in types assert "sensor" in types assert "custom" in types class TestHealthCheckerFactory: """Test HealthCheckerFactory class""" def test_factory_creation(self): """Test creating factory""" factory = HealthCheckerFactory() assert factory is not None def test_create_checker(self): """Test creating a checker""" factory = HealthCheckerFactory() checker = factory.create_checker("api", timeout=10.0) assert isinstance(checker, APIHealthChecker) assert checker.timeout == 10.0 def test_create_checker_for_service(self): """Test creating checker for specific service""" factory = HealthCheckerFactory() config = {"health_check_type": "api", "url": "http://test.local:8080"} checker = factory.create_checker_for_service("test_service", config, timeout=10.0) assert isinstance(checker, APIHealthChecker) def test_create_checker_for_service_sensor(self): """Test creating sensor checker for service""" factory = HealthCheckerFactory() config = {"health_check_type": "sensor", "url": "http://test.local:8123"} checker = factory.create_checker_for_service("test_service", config, timeout=10.0) assert isinstance(checker, SensorHealthChecker) def test_create_checker_for_service_custom(self): """Test creating custom checker for service""" factory = HealthCheckerFactory() config = {"health_check_type": "custom", "url": "http://test.local:8080"} checker = factory.create_checker_for_service("test_service", config, timeout=10.0) assert isinstance(checker, CustomHealthChecker) def test_create_checker_for_service_unknown(self): """Test creating checker for unknown service type""" factory = HealthCheckerFactory() config = {"health_check_type": "unknown", "url": "http://test.local:8080"} with pytest.raises(ValueError): factory.create_checker_for_service("test_service", config, timeout=10.0)