"""Test config validators.""" from collections import OrderedDict from datetime import date, datetime, timedelta import enum import os from socket import _GLOBAL_DEFAULT_TIMEOUT from unittest.mock import Mock, patch import uuid import pytest import voluptuous as vol import homeassistant from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, selector, template def test_boolean() -> None: """Test boolean validation.""" schema = vol.Schema(cv.boolean) for value in ( None, "T", "negative", "lock", "tr ue", [], [1, 2], {"one": "two"}, test_boolean, ): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("true", "On", "1", "YES", " true ", "enable", 1, 50, True, 0.1): assert schema(value) for value in ("false", "Off", "0", "NO", "disable", 0, False): assert not schema(value) def test_latitude() -> None: """Test latitude validation.""" schema = vol.Schema(cv.latitude) for value in ("invalid", None, -91, 91, "-91", "91", "123.01A"): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("-89", 89, "12.34"): schema(value) def test_longitude() -> None: """Test longitude validation.""" schema = vol.Schema(cv.longitude) for value in ("invalid", None, -181, 181, "-181", "181", "123.01A"): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("-179", 179, "12.34"): schema(value) def test_port() -> None: """Test TCP/UDP network port.""" schema = vol.Schema(cv.port) for value in ("invalid", None, -1, 0, 80000, "81000"): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("1000", 21, 24574): schema(value) def test_isfile() -> None: """Validate that the value is an existing file.""" schema = vol.Schema(cv.isfile) fake_file = "this-file-does-not.exist" assert not os.path.isfile(fake_file) for value in ("invalid", None, -1, 0, 80000, fake_file): with pytest.raises(vol.Invalid): schema(value) # patching methods that allow us to fake a file existing # with write access with patch("os.path.isfile", Mock(return_value=True)), patch( "os.access", Mock(return_value=True) ): schema("test.txt") def test_url() -> None: """Test URL.""" schema = vol.Schema(cv.url) for value in ( "invalid", None, 100, "htp://ha.io", "http//ha.io", "http://??,**", "https://??,**", ): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ( "http://localhost", "https://localhost/test/index.html", "http://home-assistant.io", "http://home-assistant.io/test/", "https://community.home-assistant.io/", ): assert schema(value) def test_url_no_path() -> None: """Test URL.""" schema = vol.Schema(cv.url_no_path) for value in ( "https://localhost/test/index.html", "http://home-assistant.io/test/", ): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ( "http://localhost", "http://home-assistant.io", "https://community.home-assistant.io/", ): assert schema(value) def test_platform_config() -> None: """Test platform config validation.""" options = ({}, {"hello": "world"}) for value in options: with pytest.raises(vol.MultipleInvalid): cv.PLATFORM_SCHEMA(value) options = ({"platform": "mqtt"}, {"platform": "mqtt", "beer": "yes"}) for value in options: cv.PLATFORM_SCHEMA_BASE(value) def test_ensure_list() -> None: """Test ensure_list.""" schema = vol.Schema(cv.ensure_list) assert [] == schema(None) assert [1] == schema(1) assert [1] == schema([1]) assert ["1"] == schema("1") assert ["1"] == schema(["1"]) assert [{"1": "2"}] == schema({"1": "2"}) def test_entity_id() -> None: """Test entity ID validation.""" schema = vol.Schema(cv.entity_id) with pytest.raises(vol.MultipleInvalid): schema("invalid_entity") assert schema("sensor.LIGHT") == "sensor.light" @pytest.mark.parametrize("validator", [cv.entity_ids, cv.entity_ids_or_uuids]) def test_entity_ids(validator) -> None: """Test entity ID validation.""" schema = vol.Schema(validator) options = ( "invalid_entity", "sensor.light,sensor_invalid", ["invalid_entity"], ["sensor.light", "sensor_invalid"], ["sensor.light,sensor_invalid"], ) for value in options: with pytest.raises(vol.MultipleInvalid): schema(value) options = ([], ["sensor.light"], "sensor.light") for value in options: schema(value) assert schema("sensor.LIGHT, light.kitchen ") == ["sensor.light", "light.kitchen"] def test_entity_ids_or_uuids() -> None: """Test entity ID validation.""" schema = vol.Schema(cv.entity_ids_or_uuids) valid_uuid = "a266a680b608c32770e6c45bfe6b8411" valid_uuid2 = "a266a680b608c32770e6c45bfe6b8412" invalid_uuid_capital_letters = "A266A680B608C32770E6C45bfE6B8412" options = ( "invalid_uuid", invalid_uuid_capital_letters, f"{valid_uuid},invalid_uuid", ["invalid_uuid"], [valid_uuid, "invalid_uuid"], [f"{valid_uuid},invalid_uuid"], ) for value in options: with pytest.raises(vol.MultipleInvalid): schema(value) options = ([], [valid_uuid], valid_uuid) for value in options: schema(value) assert schema(f"{valid_uuid}, {valid_uuid2} ") == [valid_uuid, valid_uuid2] def test_entity_domain() -> None: """Test entity domain validation.""" schema = vol.Schema(cv.entity_domain("sensor")) for value in ( "invalid_entity", "cover.demo", "cover.demo,sensor.another_entity", "", ): with pytest.raises(vol.MultipleInvalid): schema(value) assert schema("sensor.LIGHT") == "sensor.light" schema = vol.Schema(cv.entity_domain(("sensor", "binary_sensor"))) for value in ("invalid_entity", "cover.demo"): with pytest.raises(vol.MultipleInvalid): schema(value) assert schema("sensor.LIGHT") == "sensor.light" assert schema("binary_sensor.LIGHT") == "binary_sensor.light" def test_entities_domain() -> None: """Test entities domain validation.""" schema = vol.Schema(cv.entities_domain("sensor")) options = ( None, "", "invalid_entity", ["sensor.light", "cover.demo"], ["sensor.light", "sensor_invalid"], ) for value in options: with pytest.raises(vol.MultipleInvalid): schema(value) options = ("sensor.light", ["SENSOR.light"], ["sensor.light", "sensor.demo"]) for value in options: schema(value) assert schema("sensor.LIGHT, sensor.demo ") == ["sensor.light", "sensor.demo"] assert schema(["sensor.light", "SENSOR.demo"]) == ["sensor.light", "sensor.demo"] def test_ensure_list_csv() -> None: """Test ensure_list_csv.""" schema = vol.Schema(cv.ensure_list_csv) options = (None, 12, [], ["string"], "string1,string2") for value in options: schema(value) assert schema("string1, string2 ") == ["string1", "string2"] def test_event_schema() -> None: """Test event_schema validation.""" options = ( {}, None, {"event_data": {}}, {"event": "state_changed", "event_data": 1}, ) for value in options: with pytest.raises(vol.MultipleInvalid): cv.EVENT_SCHEMA(value) options = ( {"event": "state_changed"}, {"event": "state_changed", "event_data": {"hello": "world"}}, ) for value in options: cv.EVENT_SCHEMA(value) def test_icon() -> None: """Test icon validation.""" schema = vol.Schema(cv.icon) for value in (False, "work"): with pytest.raises(vol.MultipleInvalid): schema(value) schema("mdi:work") schema("custom:prefix") def test_time_period() -> None: """Test time_period validation.""" schema = vol.Schema(cv.time_period) options = ( None, "", "hello:world", "12:", "12:34:56:78", {}, {"wrong_key": -10}, "12.5:30", "12:30.5", "12.5:30:30", "12:30.5:30", ) for value in options: with pytest.raises(vol.MultipleInvalid): schema(value) options = ( ("8:20", timedelta(hours=8, minutes=20)), ("23:59", timedelta(hours=23, minutes=59)), ("-8:20", -1 * timedelta(hours=8, minutes=20)), ("-1:15", -1 * timedelta(hours=1, minutes=15)), ("-23:59:59", -1 * timedelta(hours=23, minutes=59, seconds=59)), ("-48:00", -1 * timedelta(days=2)), ({"minutes": 5}, timedelta(minutes=5)), (1, timedelta(seconds=1)), ("5", timedelta(seconds=5)), ("180", timedelta(seconds=180)), ("00:08:20.5", timedelta(minutes=8, seconds=20, milliseconds=500)), ("00:23:59.999", timedelta(minutes=23, seconds=59, milliseconds=999)), ("-00:08:20.5", -1 * timedelta(minutes=8, seconds=20, milliseconds=500)), ( "-12:59:59.999", -1 * timedelta(hours=12, minutes=59, seconds=59, milliseconds=999), ), ({"milliseconds": 1.5}, timedelta(milliseconds=1, microseconds=500)), ({"seconds": "1.5"}, timedelta(seconds=1, milliseconds=500)), ({"minutes": "1.5"}, timedelta(minutes=1, seconds=30)), ({"hours": -1.5}, -1 * timedelta(hours=1, minutes=30)), ({"days": "-1.5"}, -1 * timedelta(days=1, hours=12)), ) for value, result in options: assert schema(value) == result def test_remove_falsy() -> None: """Test remove falsy.""" assert cv.remove_falsy([0, None, 1, "1", {}, [], ""]) == [1, "1"] def test_service() -> None: """Test service validation.""" schema = vol.Schema(cv.service) with pytest.raises(vol.MultipleInvalid): schema("invalid_turn_on") schema("homeassistant.turn_on") def test_service_schema() -> None: """Test service_schema validation.""" options = ( {}, None, { "service": "homeassistant.turn_on", "service_template": "homeassistant.turn_on", }, {"data": {"entity_id": "light.kitchen"}}, {"service": "homeassistant.turn_on", "data": None}, { "service": "homeassistant.turn_on", "data_template": {"brightness": "{{ no_end"}, }, ) for value in options: with pytest.raises(vol.MultipleInvalid): cv.SERVICE_SCHEMA(value) options = ( {"service": "homeassistant.turn_on"}, {"service": "homeassistant.turn_on", "entity_id": "light.kitchen"}, {"service": "light.turn_on", "entity_id": "all"}, { "service": "homeassistant.turn_on", "entity_id": ["light.kitchen", "light.ceiling"], }, { "service": "light.turn_on", "entity_id": "all", "alias": "turn on kitchen lights", }, {"service": "scene.turn_on", "metadata": {}}, ) for value in options: cv.SERVICE_SCHEMA(value) # Check metadata is removed from the validated output assert cv.SERVICE_SCHEMA({"service": "scene.turn_on", "metadata": {}}) == { "service": "scene.turn_on" } def test_entity_service_schema() -> None: """Test make_entity_service_schema validation.""" schema = cv.make_entity_service_schema( {vol.Required("required"): cv.positive_int, vol.Optional("optional"): cv.string} ) options = ( {}, None, {"entity_id": "light.kitchen"}, {"optional": "value", "entity_id": "light.kitchen"}, {"required": 1}, {"required": 2, "area_id": "kitchen", "foo": "bar"}, {"required": "str", "area_id": "kitchen"}, ) for value in options: with pytest.raises(vol.MultipleInvalid): cv.SERVICE_SCHEMA(value) options = ( {"required": 1, "entity_id": "light.kitchen"}, {"required": 2, "optional": "value", "device_id": "a_device"}, {"required": 3, "area_id": "kitchen"}, ) for value in options: schema(value) options = ( { "required": 1, "entity_id": "light.kitchen", "metadata": {"some": "frontend_stuff"}, }, ) for value in options: validated = schema(value) assert "metadata" not in validated def test_entity_service_schema_with_metadata() -> None: """Test make_entity_service_schema with overridden metadata key.""" schema = cv.make_entity_service_schema({vol.Required("metadata"): cv.positive_int}) options = ({"metadata": {"some": "frontend_stuff"}, "entity_id": "light.kitchen"},) for value in options: with pytest.raises(vol.MultipleInvalid): cv.SERVICE_SCHEMA(value) options = ({"metadata": 1, "entity_id": "light.kitchen"},) for value in options: validated = schema(value) assert "metadata" in validated def test_slug() -> None: """Test slug validation.""" schema = vol.Schema(cv.slug) for value in (None, "hello world"): with pytest.raises(vol.MultipleInvalid): schema(value) for value in (12345, "hello"): schema(value) def test_string(hass: HomeAssistant) -> None: """Test string validation.""" schema = vol.Schema(cv.string) with pytest.raises(vol.Invalid): schema(None) with pytest.raises(vol.Invalid): schema([]) with pytest.raises(vol.Invalid): schema({}) for value in (True, 1, "hello"): schema(value) # Test template support for text, native in ( ("[1, 2]", [1, 2]), ("{1, 2}", {1, 2}), ("(1, 2)", (1, 2)), ('{"hello": True}', {"hello": True}), ): tpl = template.Template(text, hass) result = tpl.async_render() assert isinstance(result, template.ResultWrapper) assert result == native assert schema(result) == text def test_string_with_no_html() -> None: """Test string with no html validation.""" schema = vol.Schema(cv.string_with_no_html) with pytest.raises(vol.Invalid): schema("This has HTML in it Link") with pytest.raises(vol.Invalid): schema("Bold") for value in ( True, 3, "Hello", "**Hello**", "This has no HTML [Link](https://home-assistant.io)", ): schema(value) def test_temperature_unit() -> None: """Test temperature unit validation.""" schema = vol.Schema(cv.temperature_unit) with pytest.raises(vol.MultipleInvalid): schema("K") schema("C") schema("F") def test_x10_address() -> None: """Test x10 addr validator.""" schema = vol.Schema(cv.x10_address) with pytest.raises(vol.Invalid): schema("Q1") schema("q55") schema("garbage_addr") schema("a1") schema("C11") def test_template(hass: HomeAssistant) -> None: """Test template validator.""" schema = vol.Schema(cv.template) for value in ( None, "{{ partial_print }", "{% if True %}Hello", ["test"], ): with pytest.raises(vol.Invalid): schema(value) options = ( 1, "Hello", "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", # Function added as an extension by Home Assistant "{{ expand('group.foo')|map(attribute='entity_id')|list }}", # Filter added as an extension by Home Assistant "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", ) for value in options: schema(value) async def test_template_no_hass(hass: HomeAssistant) -> None: """Test template validator.""" schema = vol.Schema(cv.template) for value in ( None, "{{ partial_print }", "{% if True %}Hello", ["test"], # Filter added as an extension by Home Assistant "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", ): with pytest.raises(vol.Invalid): await hass.async_add_executor_job(schema, value) options = ( 1, "Hello", "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", # Function added as an extension by Home Assistant "{{ expand('group.foo')|map(attribute='entity_id')|list }}", ) for value in options: await hass.async_add_executor_job(schema, value) def test_dynamic_template(hass: HomeAssistant) -> None: """Test dynamic template validator.""" schema = vol.Schema(cv.dynamic_template) for value in ( None, 1, "{{ partial_print }", "{% if True %}Hello", ["test"], "just a string", ): with pytest.raises(vol.Invalid): schema(value) options = ( "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", # Function added as an extension by Home Assistant "{{ expand('group.foo')|map(attribute='entity_id')|list }}", # Filter added as an extension by Home Assistant "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", ) for value in options: schema(value) async def test_dynamic_template_no_hass(hass: HomeAssistant) -> None: """Test dynamic template validator.""" schema = vol.Schema(cv.dynamic_template) for value in ( None, 1, "{{ partial_print }", "{% if True %}Hello", ["test"], "just a string", # Filter added as an extension by Home Assistant "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", ): with pytest.raises(vol.Invalid): await hass.async_add_executor_job(schema, value) options = ( "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", # Function added as an extension by Home Assistant "{{ expand('group.foo')|map(attribute='entity_id')|list }}", ) for value in options: await hass.async_add_executor_job(schema, value) def test_template_complex() -> None: """Test template_complex validator.""" schema = vol.Schema(cv.template_complex) for value in ("{{ partial_print }", "{% if True %}Hello"): with pytest.raises(vol.MultipleInvalid): schema(value) options = ( 1, "Hello", "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", {"test": 1, "test2": "{{ beer }}"}, ["{{ beer }}", 1], ) for value in options: schema(value) # ensure the validator didn't mutate the input assert options == ( 1, "Hello", "{{ beer }}", "{% if 1 == 1 %}Hello{% else %}World{% endif %}", {"test": 1, "test2": "{{ beer }}"}, ["{{ beer }}", 1], ) # Ensure we don't mutate non-string types that cannot be templates. for value in (1, True, None): assert schema(value) == value def test_time_zone() -> None: """Test time zone validation.""" schema = vol.Schema(cv.time_zone) with pytest.raises(vol.MultipleInvalid): schema("America/Do_Not_Exist") schema("America/Los_Angeles") schema("UTC") def test_date() -> None: """Test date validation.""" schema = vol.Schema(cv.date) for value in ["Not a date", "23:42", "2016-11-23T18:59:08"]: with pytest.raises(vol.Invalid): schema(value) schema(datetime.now().date()) schema("2016-11-23") def test_time() -> None: """Test date validation.""" schema = vol.Schema(cv.time) for value in ["Not a time", "2016-11-23", "2016-11-23T18:59:08"]: with pytest.raises(vol.Invalid): schema(value) schema(datetime.now().time()) schema("23:42:00") schema("23:42") def test_datetime() -> None: """Test date time validation.""" schema = vol.Schema(cv.datetime) for value in [date.today(), "Wrong DateTime"]: with pytest.raises(vol.MultipleInvalid): schema(value) schema(datetime.now()) schema("2016-11-23T18:59:08") def test_multi_select() -> None: """Test multi select validation. Expected behavior: - Will not accept any input but a list - Will not accept selections outside of configured scope """ schema = vol.Schema(cv.multi_select({"paulus": "Paulus", "robban": "Robban"})) with pytest.raises(vol.Invalid): schema("robban") schema(["paulus", "martinhj"]) schema(["robban", "paulus"]) def test_multi_select_in_serializer() -> None: """Test multi_select with custom_serializer.""" assert cv.custom_serializer(cv.multi_select({"paulus": "Paulus"})) == { "type": "multi_select", "options": {"paulus": "Paulus"}, } def test_boolean_in_serializer() -> None: """Test boolean with custom_serializer.""" assert cv.custom_serializer(cv.boolean) == { "type": "boolean", } def test_string_in_serializer() -> None: """Test string with custom_serializer.""" assert cv.custom_serializer(cv.string) == { "type": "string", } def test_selector_in_serializer() -> None: """Test selector with custom_serializer.""" assert cv.custom_serializer(selector.selector({"text": {}})) == { "selector": { "text": { "multiline": False, } } } def test_positive_time_period_dict_in_serializer() -> None: """Test positive_time_period_dict with custom_serializer.""" assert cv.custom_serializer(cv.positive_time_period_dict) == { "type": "positive_time_period_dict", } @pytest.fixture def schema(): """Create a schema used for testing deprecation.""" return vol.Schema({"venus": cv.boolean, "mars": cv.boolean, "jupiter": cv.boolean}) @pytest.fixture def version(monkeypatch): """Patch the version used for testing to 0.5.0.""" monkeypatch.setattr(homeassistant.const, "__version__", "0.5.0") def test_deprecated_with_no_optionals(caplog: pytest.LogCaptureFixture, schema) -> None: """Test deprecation behaves correctly when optional params are None. Expected behavior: - Outputs the appropriate deprecation warning if key is detected - Processes schema without changing any values - No warning or difference in output if key is not provided """ deprecated_schema = vol.All(cv.deprecated("mars"), schema) test_data = {"mars": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert caplog.records[0].name in [ __name__, "homeassistant.helpers.config_validation", ] assert ( "The 'mars' option is deprecated, please remove it from your configuration" ) in caplog.text assert test_data == output caplog.clear() assert len(caplog.records) == 0 test_data = {"venus": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 assert test_data == output def test_deprecated_or_removed_param_and_raise( caplog: pytest.LogCaptureFixture, schema ) -> None: """Test removed or deprecation options and fail the config validation by raising an exception. Expected behavior: - Outputs the appropriate deprecation or removed from support error if key is detected """ removed_schema = vol.All(cv.deprecated("mars", raise_if_present=True), schema) test_data = {"mars": True} with pytest.raises(vol.Invalid) as excinfo: removed_schema(test_data) assert ( "The 'mars' option is deprecated, please remove it from your configuration" in str(excinfo.value) ) assert len(caplog.records) == 0 test_data = {"venus": True} output = removed_schema(test_data.copy()) assert len(caplog.records) == 0 assert test_data == output deprecated_schema = vol.All(cv.removed("mars"), schema) test_data = {"mars": True} with pytest.raises(vol.Invalid) as excinfo: deprecated_schema(test_data) assert ( "The 'mars' option has been removed, please remove it from your configuration" in str(excinfo.value) ) assert len(caplog.records) == 0 test_data = {"venus": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 assert test_data == output def test_deprecated_with_replacement_key( caplog: pytest.LogCaptureFixture, schema ) -> None: """Test deprecation behaves correctly when only a replacement key is provided. Expected behavior: - Outputs the appropriate deprecation warning if key is detected - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided - No warning or difference in output if neither key nor replacement_key are provided """ deprecated_schema = vol.All( cv.deprecated("mars", replacement_key="jupiter"), schema ) test_data = {"mars": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text assert {"jupiter": True} == output caplog.clear() assert len(caplog.records) == 0 test_data = {"jupiter": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 assert test_data == output test_data = {"venus": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 assert test_data == output def test_deprecated_with_default(caplog: pytest.LogCaptureFixture, schema) -> None: """Test deprecation behaves correctly with a default value. This is likely a scenario that would never occur. Expected behavior: - Behaves identically as when the default value was not present """ deprecated_schema = vol.All(cv.deprecated("mars", default=False), schema) test_data = {"mars": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert caplog.records[0].name == __name__ assert ( "The 'mars' option is deprecated, please remove it from your configuration" ) in caplog.text assert test_data == output caplog.clear() assert len(caplog.records) == 0 test_data = {"venus": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 assert test_data == output def test_deprecated_with_replacement_key_and_default( caplog: pytest.LogCaptureFixture, schema ) -> None: """Test deprecation with a replacement key and default. Expected behavior: - Outputs the appropriate deprecation warning if key is detected - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case """ deprecated_schema = vol.All( cv.deprecated("mars", replacement_key="jupiter", default=False), schema ) test_data = {"mars": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text assert {"jupiter": True} == output caplog.clear() assert len(caplog.records) == 0 test_data = {"jupiter": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 assert test_data == output test_data = {"venus": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 0 assert {"venus": True, "jupiter": False} == output deprecated_schema_with_default = vol.All( vol.Schema( { "venus": cv.boolean, vol.Optional("mars", default=False): cv.boolean, vol.Optional("jupiter", default=False): cv.boolean, } ), cv.deprecated("mars", replacement_key="jupiter", default=False), ) test_data = {"mars": True} output = deprecated_schema_with_default(test_data.copy()) assert len(caplog.records) == 1 assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text assert {"jupiter": True} == output def test_deprecated_cant_find_module() -> None: """Test if the current module cannot be inspected.""" with patch("inspect.getmodule", return_value=None): # This used to raise. cv.deprecated( "mars", replacement_key="jupiter", default=False, ) with patch("inspect.getmodule", return_value=None): # This used to raise. cv.removed( "mars", default=False, ) def test_deprecated_or_removed_logger_with_config_attributes( caplog: pytest.LogCaptureFixture, ) -> None: """Test if the logger outputs the correct message if the line and file attribute is available in config.""" file: str = "configuration.yaml" line: int = 54 # test as deprecated option replacement_key = "jupiter" option_status = "is deprecated" replacement = f"'mars' option near {file}:{line} {option_status}, please replace it with '{replacement_key}'" config = OrderedDict([("mars", "blah")]) setattr(config, "__config_file__", file) setattr(config, "__line__", line) cv.deprecated("mars", replacement_key=replacement_key, default=False)(config) assert len(caplog.records) == 1 assert replacement in caplog.text caplog.clear() assert len(caplog.records) == 0 # test as removed option option_status = "has been removed" replacement = f"'mars' option near {file}:{line} {option_status}, please remove it from your configuration" config = OrderedDict([("mars", "blah")]) setattr(config, "__config_file__", file) setattr(config, "__line__", line) cv.removed("mars", default=False, raise_if_present=False)(config) assert len(caplog.records) == 1 assert replacement in caplog.text caplog.clear() assert len(caplog.records) == 0 def test_deprecated_logger_with_one_config_attribute( caplog: pytest.LogCaptureFixture, ) -> None: """Test if the logger outputs the correct message if only one of line and file attribute is available in config.""" file: str = "configuration.yaml" line: int = 54 replacement = f"'mars' option near {file}:{line} is deprecated" config = OrderedDict([("mars", "blah")]) setattr(config, "__config_file__", file) cv.deprecated("mars", replacement_key="jupiter", default=False)(config) assert len(caplog.records) == 1 assert replacement not in caplog.text assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text caplog.clear() assert len(caplog.records) == 0 config = OrderedDict([("mars", "blah")]) setattr(config, "__line__", line) cv.deprecated("mars", replacement_key="jupiter", default=False)(config) assert len(caplog.records) == 1 assert replacement not in caplog.text assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text caplog.clear() assert len(caplog.records) == 0 def test_deprecated_logger_without_config_attributes( caplog: pytest.LogCaptureFixture, ) -> None: """Test if the logger outputs the correct message if the line and file attribute is not available in config.""" file: str = "configuration.yaml" line: int = 54 replacement = f"'mars' option near {file}:{line} is deprecated" config = OrderedDict([("mars", "blah")]) cv.deprecated("mars", replacement_key="jupiter", default=False)(config) assert len(caplog.records) == 1 assert replacement not in caplog.text assert ( "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text caplog.clear() assert len(caplog.records) == 0 def test_key_dependency() -> None: """Test key_dependency validator.""" schema = vol.Schema(cv.key_dependency("beer", "soda")) options = {"beer": None} for value in options: with pytest.raises(vol.MultipleInvalid): schema(value) options = ({"beer": None, "soda": None}, {"soda": None}, {}) for value in options: schema(value) def test_has_at_most_one_key() -> None: """Test has_at_most_one_key validator.""" schema = vol.Schema(cv.has_at_most_one_key("beer", "soda")) for value in (None, [], {"beer": None, "soda": None}): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ({}, {"beer": None}, {"soda": None}, {vol.Optional("soda"): None}): schema(value) def test_has_at_least_one_key() -> None: """Test has_at_least_one_key validator.""" schema = vol.Schema(cv.has_at_least_one_key("beer", "soda")) for value in (None, [], {}, {"wine": None}): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ({"beer": None}, {"soda": None}, {vol.Required("soda"): None}): schema(value) def test_enum() -> None: """Test enum validator.""" class TestEnum(enum.Enum): """Test enum.""" value1 = "Value 1" value2 = "Value 2" schema = vol.Schema(cv.enum(TestEnum)) with pytest.raises(vol.Invalid): schema("value3") def test_socket_timeout(): # pylint: disable=invalid-name """Test socket timeout validator.""" schema = vol.Schema(cv.socket_timeout) with pytest.raises(vol.Invalid): schema(0.0) with pytest.raises(vol.Invalid): schema(-1) assert schema(None) == _GLOBAL_DEFAULT_TIMEOUT assert schema(1) == 1.0 def test_matches_regex() -> None: """Test matches_regex validator.""" schema = vol.Schema(cv.matches_regex(".*uiae.*")) with pytest.raises(vol.Invalid): schema(1.0) with pytest.raises(vol.Invalid): schema(" nrtd ") test_str = "This is a test including uiae." assert schema(test_str) == test_str def test_is_regex() -> None: """Test the is_regex validator.""" schema = vol.Schema(cv.is_regex) with pytest.raises(vol.Invalid): schema("(") with pytest.raises(vol.Invalid): schema({"a dict": "is not a regex"}) valid_re = ".*" schema(valid_re) def test_comp_entity_ids() -> None: """Test config validation for component entity IDs.""" schema = vol.Schema(cv.comp_entity_ids) for valid in ( "ALL", "all", "AlL", "light.kitchen", ["light.kitchen"], ["light.kitchen", "light.ceiling"], [], ): schema(valid) for invalid in (["light.kitchen", "not-entity-id"], "*", ""): with pytest.raises(vol.Invalid): schema(invalid) def test_uuid4_hex(caplog: pytest.LogCaptureFixture) -> None: """Test uuid validation.""" schema = vol.Schema(cv.uuid4_hex) for value in ["Not a hex string", "0", 0]: with pytest.raises(vol.Invalid): schema(value) with pytest.raises(vol.Invalid): # the 13th char should be 4 schema("a03d31b22eee1acc9b90eec40be6ed23") with pytest.raises(vol.Invalid): # the 17th char should be 8-a schema("a03d31b22eee4acc7b90eec40be6ed23") _hex = uuid.uuid4().hex assert schema(_hex) == _hex assert schema(_hex.upper()) == _hex def test_key_value_schemas() -> None: """Test key value schemas.""" schema = vol.Schema( cv.key_value_schemas( "mode", { "number": vol.Schema({"mode": "number", "data": int}), "string": vol.Schema({"mode": "string", "data": str}), }, ) ) with pytest.raises(vol.Invalid) as excinfo: schema(True) assert str(excinfo.value) == "Expected a dictionary" for mode in None, {"a": "dict"}, "invalid": with pytest.raises(vol.Invalid) as excinfo: schema({"mode": mode}) assert ( str(excinfo.value) == f"Unexpected value for mode: '{mode}'. Expected number, string" ) with pytest.raises(vol.Invalid) as excinfo: schema({"mode": "number", "data": "string-value"}) assert str(excinfo.value) == "expected int for dictionary value @ data['data']" with pytest.raises(vol.Invalid) as excinfo: schema({"mode": "string", "data": 1}) assert str(excinfo.value) == "expected str for dictionary value @ data['data']" for mode, data in (("number", 1), ("string", "hello")): schema({"mode": mode, "data": data}) def test_key_value_schemas_with_default() -> None: """Test key value schemas.""" schema = vol.Schema( cv.key_value_schemas( "mode", { "number": vol.Schema({"mode": "number", "data": int}), "string": vol.Schema({"mode": "string", "data": str}), }, vol.Schema({"mode": cv.dynamic_template}), "a cool template", ) ) with pytest.raises(vol.Invalid) as excinfo: schema(True) assert str(excinfo.value) == "Expected a dictionary" for mode in None, {"a": "dict"}, "invalid": with pytest.raises(vol.Invalid) as excinfo: schema({"mode": mode}) assert ( str(excinfo.value) == f"Unexpected value for mode: '{mode}'. Expected number, string, a cool template" ) with pytest.raises(vol.Invalid) as excinfo: schema({"mode": "number", "data": "string-value"}) assert str(excinfo.value) == "expected int for dictionary value @ data['data']" with pytest.raises(vol.Invalid) as excinfo: schema({"mode": "string", "data": 1}) assert str(excinfo.value) == "expected str for dictionary value @ data['data']" for mode, data in (("number", 1), ("string", "hello")): schema({"mode": mode, "data": data}) schema({"mode": "{{ 1 + 1}}"}) def test_script(caplog: pytest.LogCaptureFixture) -> None: """Test script validation is user friendly.""" for data, msg in ( ({"delay": "{{ invalid"}, "should be format 'HH:MM'"), ({"wait_template": "{{ invalid"}, "invalid template"), ({"condition": "invalid"}, "Unexpected value for condition: 'invalid'"), ( {"condition": "not", "conditions": {"condition": "invalid"}}, "Unexpected value for condition: 'invalid'", ), # The validation error message could be improved to explain that this is not # a valid shorthand template ( {"condition": "not", "conditions": "not a dynamic template"}, "Expected a dictionary", ), ({"event": None}, "string value is None for dictionary value @ data['event']"), ( {"device_id": None}, "string value is None for dictionary value @ data['device_id']", ), ( {"scene": "light.kitchen"}, "Entity ID 'light.kitchen' does not belong to domain 'scene'", ), ): with pytest.raises(vol.Invalid) as excinfo: cv.script_action(data) assert msg in str(excinfo.value) def test_whitespace() -> None: """Test whitespace validation.""" schema = vol.Schema(cv.whitespace) for value in ( None, "" "T", "negative", "lock", "tr ue", [], [1, 2], {"one": "two"}, ): with pytest.raises(vol.MultipleInvalid): schema(value) for value in (" ", " "): assert schema(value) def test_currency() -> None: """Test currency validator.""" schema = vol.Schema(cv.currency) for value in ( None, "BTC", ): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("EUR", "USD"): assert schema(value) def test_historic_currency() -> None: """Test historic currency validator.""" schema = vol.Schema(cv.historic_currency) for value in (None, "BTC", "EUR"): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("DEM", "NLG"): assert schema(value) def test_country() -> None: """Test country validator.""" schema = vol.Schema(cv.country) for value in (None, "Candyland", "USA"): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("NL", "SE"): assert schema(value) def test_language() -> None: """Test language validator.""" schema = vol.Schema(cv.language) for value in (None, "Klingon", "english"): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ("en", "sv"): assert schema(value) def test_positive_time_period_template() -> None: """Test positive time period template validation.""" schema = vol.Schema(cv.positive_time_period_template) with pytest.raises(vol.MultipleInvalid): schema({}) with pytest.raises(vol.MultipleInvalid): schema({5: 5}) with pytest.raises(vol.MultipleInvalid): schema({"invalid": 5}) with pytest.raises(vol.MultipleInvalid): schema("invalid") # Time periods pass schema("00:01") schema("00:00:01") schema("00:00:00.500") schema({"minutes": 5}) # Templates are not evaluated and will pass schema("{{ 'invalid' }}") schema({"{{ 'invalid' }}": 5}) schema({"minutes": "{{ 'invalid' }}"})