core/tests/helpers/test_check_config.py

592 lines
20 KiB
Python

"""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."""
_LOGGER.debug("CONFIG - %s lines - %s errors", len(conf), len(conf.errors))
for cnt, (key, val) in enumerate(conf.items()):
_LOGGER.debug("#%s - %s: %s", cnt + 1, key, val)
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.async_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.async_get_platform",
side_effect=ImportError("blablabla"),
),
patch("os.path.isfile", return_value=True),
patch("homeassistant.loader.Integration.platforms_exists", 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.async_get_platform",
side_effect=[None, ImportError("blablabla")],
),
patch("homeassistant.loader.Integration.platforms_exists", return_value=True),
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 platform setup with an invalid package config."""
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_package_definition_invalid_slug_keys(hass: HomeAssistant) -> None:
"""Test a platform setup with a broken package: keys must be slugs."""
files = {
YAML_CONFIG_FILE: BASE_CONFIG
+ ' packages:\n not a slug:\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 'not a slug' failed: Invalid package definition 'not a slug': invalid slug not a "
"slug (try not_a_slug). Package will not be initialized"
),
"homeassistant.packages.not a slug",
{"group": ["a"]},
)
_assert_warnings_errors(res, [warning], [])
async def test_package_definition_invalid_dict(hass: HomeAssistant) -> None:
"""Test a platform setup with a broken package: packages must be dicts."""
files = {
YAML_CONFIG_FILE: BASE_CONFIG
+ ' packages:\n not_a_dict:\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 'not_a_dict' failed: Invalid package definition 'not_a_dict': expected a "
"dictionary. Package will not be initialized"
),
"homeassistant.packages.not_a_dict",
[{"group": ["a"]}],
)
_assert_warnings_errors(res, [warning], [])
async def test_package_schema_invalid(hass: HomeAssistant) -> None:
"""Test an invalid platform config because of severely broken packages section."""
files = {
YAML_CONFIG_FILE: "homeassistant:\n packages:\n - must\n - not\n - be\n - a\n - list"
}
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:"
" expected a dictionary for dictionary value 'packages', got ['must', 'not', 'be', 'a', 'list']"
),
"homeassistant",
{"packages": ["must", "not", "be", "a", "list"]},
)
_assert_warnings_errors(res, [], [error])
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, [], [])