"""Test check_config helper.""" import logging from unittest.mock import Mock, patch import pytest import voluptuous as vol from homeassistant.config import YAML_CONFIG_FILE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.check_config import ( CheckConfigError, HomeAssistantConfig, async_check_ha_config_file, ) import homeassistant.helpers.config_validation as cv from homeassistant.requirements import RequirementsNotFound from tests.common import ( MockModule, MockPlatform, mock_integration, mock_platform, patch_yaml_files, ) _LOGGER = logging.getLogger(__name__) BASE_CONFIG = ( "homeassistant:\n" " name: Home\n" " latitude: -26.107361\n" " longitude: 28.054500\n" " elevation: 1600\n" " unit_system: metric\n" " time_zone: GMT\n" "\n\n" ) BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" def log_ha_config(conf): """Log the returned config.""" cnt = 0 _LOGGER.debug("CONFIG - %s lines - %s errors", len(conf), len(conf.errors)) for key, val in conf.items(): _LOGGER.debug("#%s - %s: %s", cnt, key, val) cnt += 1 for cnt, err in enumerate(conf.errors): _LOGGER.debug("error[%s] = %s", cnt, err) def _assert_warnings_errors( res: HomeAssistantConfig, expected_warnings: list[CheckConfigError], expected_errors: list[CheckConfigError], ) -> None: assert len(res.warnings) == len(expected_warnings) assert len(res.errors) == len(expected_errors) expected_warning_str = "" expected_error_str = "" for idx, expected_warning in enumerate(expected_warnings): assert res.warnings[idx] == expected_warning expected_warning_str += expected_warning.message assert res.warning_str == expected_warning_str for idx, expected_error in enumerate(expected_errors): assert res.errors[idx] == expected_error expected_error_str += expected_error.message assert res.error_str == expected_error_str async def test_bad_core_config(hass: HomeAssistant) -> None: """Test a bad core config setup.""" files = {YAML_CONFIG_FILE: BAD_CORE_CONFIG} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) error = CheckConfigError( ( f"Invalid config for 'homeassistant' at {YAML_CONFIG_FILE}, line 2:" " not a valid value for dictionary value 'unit_system', got 'bad'" ), "homeassistant", {"unit_system": "bad"}, ) _assert_warnings_errors(res, [], [error]) async def test_config_platform_valid(hass: HomeAssistant) -> None: """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant", "light"} assert res["light"] == [{"platform": "demo"}] _assert_warnings_errors(res, [], []) async def test_integration_not_found(hass: HomeAssistant) -> None: """Test errors if integration not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} warning = CheckConfigError( "Integration error: beer - Integration 'beer' not found.", None, None ) _assert_warnings_errors(res, [warning], []) async def test_integrationt_requirement_not_found(hass: HomeAssistant) -> None: """Test errors if integration with a requirement not found not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "test_custom_component:"} with patch( "homeassistant.helpers.check_config.async_get_integration_with_requirements", side_effect=RequirementsNotFound("test_custom_component", ["any"]), ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} warning = CheckConfigError( ( "Integration error: test_custom_component - Requirements for" " test_custom_component not found: ['any']." ), None, None, ) _assert_warnings_errors(res, [warning], []) async def test_integration_not_found_recovery_mode(hass: HomeAssistant) -> None: """Test no errors if integration not found in recovery mode.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"} hass.config.recovery_mode = True with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} _assert_warnings_errors(res, [], []) async def test_integration_not_found_safe_mode(hass: HomeAssistant) -> None: """Test no errors if integration not found in safe mode.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"} hass.config.safe_mode = True with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} _assert_warnings_errors(res, [], []) async def test_integration_import_error(hass: HomeAssistant) -> None: """Test errors if integration with a requirement not found not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:"} with patch( "homeassistant.loader.Integration.get_component", side_effect=ImportError("blablabla"), ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} warning = CheckConfigError( "Component error: light - blablabla", None, None, ) _assert_warnings_errors(res, [warning], []) @pytest.mark.parametrize( ("integration", "errors", "warnings", "message"), [ ("frontend", 1, 0, "'blah' is an invalid option for 'frontend'"), ("http", 1, 0, "'blah' is an invalid option for 'http'"), ("logger", 0, 1, "'blah' is an invalid option for 'logger'"), ], ) async def test_integration_schema_error( hass: HomeAssistant, integration: str, errors: int, warnings: int, message: str ) -> None: """Test schema error in integration.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + f"frontend:\n{integration}:\n blah:"} hass.config.safe_mode = True with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert len(res.errors) == errors assert len(res.warnings) == warnings for err in res.errors: assert message in err.message for warn in res.warnings: assert message in warn.message async def test_platform_not_found(hass: HomeAssistant) -> None: """Test errors if platform not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: beer"} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant", "light"} assert res["light"] == [] warning = CheckConfigError( ( "Platform error 'light' from integration 'beer' - " "Integration 'beer' not found." ), None, None, ) _assert_warnings_errors(res, [warning], []) async def test_platform_not_found_recovery_mode(hass: HomeAssistant) -> None: """Test no errors if platform not found in recovery mode.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: beer"} hass.config.recovery_mode = True with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant", "light"} assert res["light"] == [] _assert_warnings_errors(res, [], []) async def test_platform_not_found_safe_mode(hass: HomeAssistant) -> None: """Test no errors if platform not found in safe mode.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: beer"} hass.config.safe_mode = True with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant", "light"} assert res["light"] == [] _assert_warnings_errors(res, [], []) @pytest.mark.parametrize( ("extra_config", "warnings", "message", "config"), [ ( "blah:\n - platform: test\n option1: abc", 0, None, None, ), ( "blah:\n - platform: test\n option1: 123", 1, "expected str for dictionary value", {"option1": 123, "platform": "test"}, ), # Test the attached config is unvalidated (key old is removed by validator) ( "blah:\n - platform: test\n old: blah\n option1: 123", 1, "expected str for dictionary value", {"old": "blah", "option1": 123, "platform": "test"}, ), # Test base platform configuration error ( "blah:\n - paltfrom: test\n", 1, "required key 'platform' not provided", {"paltfrom": "test"}, ), ], ) async def test_platform_schema_error( hass: HomeAssistant, extra_config: str, warnings: int, message: str | None, config: dict | None, ) -> None: """Test schema error in platform.""" comp_platform_schema = cv.PLATFORM_SCHEMA.extend({vol.Remove("old"): str}) comp_platform_schema_base = comp_platform_schema.extend({}, extra=vol.ALLOW_EXTRA) mock_integration( hass, MockModule("blah", platform_schema_base=comp_platform_schema_base), ) test_platform_schema = comp_platform_schema.extend({"option1": str}) mock_platform( hass, "test.blah", MockPlatform(platform_schema=test_platform_schema), ) files = {YAML_CONFIG_FILE: BASE_CONFIG + extra_config} hass.config.safe_mode = True with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert len(res.errors) == 0 assert len(res.warnings) == warnings for warn in res.warnings: assert message in warn.message assert warn.config == config async def test_config_platform_import_error(hass: HomeAssistant) -> None: """Test errors if config platform fails to import.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: beer"} with patch( "homeassistant.loader.Integration.get_platform", side_effect=ImportError("blablabla"), ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} error = CheckConfigError( "Error importing config platform light: blablabla", None, None, ) _assert_warnings_errors(res, [], [error]) async def test_platform_import_error(hass: HomeAssistant) -> None: """Test errors if platform not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"} with patch( "homeassistant.loader.Integration.get_platform", side_effect=[None, ImportError("blablabla")], ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant", "light"} warning = CheckConfigError( "Platform error 'light' from integration 'demo' - blablabla", None, None, ) _assert_warnings_errors(res, [warning], []) async def test_package_invalid(hass: HomeAssistant) -> None: """Test a valid platform setup.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + ' packages:\n p1:\n group: ["a"]'} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} warning = CheckConfigError( ( "Setup of package 'p1' failed: integration 'group' cannot be merged" ", expected a dict" ), "homeassistant.packages.p1.group", {"group": ["a"]}, ) _assert_warnings_errors(res, [warning], []) async def test_missing_included_file(hass: HomeAssistant) -> None: """Test missing included file.""" files = {YAML_CONFIG_FILE: BASE_CONFIG + "automation: !include no.yaml"} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert len(res.errors) == 1 assert len(res.warnings) == 0 assert res.errors[0].message.startswith("Error loading") assert res.errors[0].domain is None assert res.errors[0].config is None async def test_automation_config_platform(hass: HomeAssistant) -> None: """Test automation async config.""" files = { YAML_CONFIG_FILE: BASE_CONFIG + """ automation: use_blueprint: path: test_event_service.yaml input: trigger_event: blueprint_event service_to_call: test.automation input_datetime: """, hass.config.path("blueprints/automation/test_event_service.yaml"): """ blueprint: name: "Call service based on event" domain: automation input: trigger_event: service_to_call: trigger: platform: event event_type: !input trigger_event action: service: !input service_to_call """, } with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) assert len(res.get("automation", [])) == 1 assert len(res.errors) == 0 assert len(res.warnings) == 0 assert "input_datetime" in res @pytest.mark.parametrize( ("exception", "errors", "warnings", "message"), [ ( Exception("Broken"), 1, 0, "Unexpected error calling config validator: Broken", ), ( HomeAssistantError("Broken"), 0, 1, "Invalid config for 'bla' at configuration.yaml, line 11: Broken", ), ], ) async def test_config_platform_raise( hass: HomeAssistant, exception: Exception, errors: int, warnings: int, message: str, ) -> None: """Test bad config validation platform.""" mock_platform( hass, "bla.config", Mock(async_validate_config=Mock(side_effect=exception)), ) files = { YAML_CONFIG_FILE: BASE_CONFIG + """ bla: value: 1 """, } with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) error = CheckConfigError( message, "bla", {"value": 1}, ) _assert_warnings_errors(res, [error] * warnings, [error] * errors) async def test_removed_yaml_support(hass: HomeAssistant) -> None: """Test config validation check with removed CONFIG_SCHEMA without raise if present.""" mock_integration( hass, MockModule( domain="bla", config_schema=cv.removed("bla", raise_if_present=False) ), False, ) files = {YAML_CONFIG_FILE: BASE_CONFIG + "bla:\n platform: demo"} with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) log_ha_config(res) assert res.keys() == {"homeassistant"} _assert_warnings_errors(res, [], [])