mirror of https://github.com/home-assistant/core
592 lines
20 KiB
Python
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, [], [])
|