core/tests/helpers/test_check_config.py
J. Nick Koston 8fe80a4766
Migrate remaining get_platform in check_config to async_get_platform (#112470)
These were very likely to be cached so they were low on the
list to migrate, but since they are called in the event loop
its best to be sure we do no blocking I/O
2024-03-05 23:47:41 -05:00

575 lines
19 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."""
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.async_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.async_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 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, [], [])