From 3187506eb95d561baad71030848e4bf1482bf242 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Sat, 6 Sep 2025 13:49:03 +0200 Subject: [PATCH] Ignore incorrect themes (#151794) --- homeassistant/components/frontend/__init__.py | 40 +++++++++++++++++-- tests/components/frontend/test_init.py | 32 +++++++++++---- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index cf54565fe03b..2d39726abbfe 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -38,6 +38,8 @@ from homeassistant.util.hass_dict import HassKey from .storage import async_setup_frontend_storage +_LOGGER = logging.getLogger(__name__) + DOMAIN = "frontend" CONF_THEMES = "themes" CONF_THEMES_MODES = "modes" @@ -73,7 +75,23 @@ VALUE_NO_THEME = "none" PRIMARY_COLOR = "primary-color" -_LOGGER = logging.getLogger(__name__) + +LEGACY_THEME_SCHEMA = vol.Any( + # Legacy theme scheme + {cv.string: cv.string}, + # New extended schema with mode support + { + # Theme variables that apply to all modes + cv.string: cv.string, + # Mode specific theme variables + vol.Optional(CONF_THEMES_MODES): vol.Schema( + { + vol.Optional(CONF_THEMES_LIGHT): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_THEMES_DARK): vol.Schema({cv.string: cv.string}), + } + ), + }, +) THEME_SCHEMA = vol.Schema( { @@ -90,14 +108,28 @@ THEME_SCHEMA = vol.Schema( } ) -THEMES_SCHEMA = vol.Schema({cv.string: THEME_SCHEMA}) + +def _validate_themes(themes: dict) -> dict[str, Any]: + """Validate themes.""" + validated_themes = {} + for theme_name, theme in themes.items(): + theme_name = cv.string(theme_name) + LEGACY_THEME_SCHEMA(theme) + + try: + validated_themes[theme_name] = THEME_SCHEMA(theme) + except vol.Invalid as err: + _LOGGER.error("Theme %s is invalid: %s", theme_name, err) + + return validated_themes + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Optional(CONF_FRONTEND_REPO): cv.isdir, - vol.Optional(CONF_THEMES): THEMES_SCHEMA, + vol.Optional(CONF_THEMES): vol.All(dict, _validate_themes), vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( cv.ensure_list, [cv.string] ), @@ -536,7 +568,7 @@ async def _async_setup_themes( new_themes = config.get(DOMAIN, {}).get(CONF_THEMES, {}) try: - THEMES_SCHEMA(new_themes) + new_themes = _validate_themes(new_themes) except vol.Invalid as err: raise HomeAssistantError(f"Failed to reload themes: {err}") from err diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 5695a3bca15a..e5cd6fa3089e 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,6 +1,7 @@ """The tests for Home Assistant frontend.""" from collections.abc import Generator +from contextlib import nullcontext from http import HTTPStatus from pathlib import Path import re @@ -411,13 +412,14 @@ async def test_themes_reload_themes( @pytest.mark.usefixtures("frontend") @pytest.mark.parametrize( - ("invalid_theme", "error"), + ("invalid_theme", "error", "log"), [ ( { "invalid0": "blue", }, "expected a dictionary", + None, ), ( { @@ -426,13 +428,15 @@ async def test_themes_reload_themes( "modes": "light:{} dark:{}", } }, - "expected a dictionary.*modes", + None, + "expected a dictionary", ), ( { "invalid2": None, }, "expected a dictionary", + None, ), ( { @@ -441,7 +445,8 @@ async def test_themes_reload_themes( "modes": {}, } }, - "at least one of light, dark.*modes", + None, + "must contain at least one of light, dark", ), ( { @@ -450,7 +455,8 @@ async def test_themes_reload_themes( "modes": None, } }, - "expected a dictionary.*modes", + "string value is None for dictionary value", + None, ), ( { @@ -460,6 +466,7 @@ async def test_themes_reload_themes( } }, "extra keys not allowed.*dank", + None, ), ], ) @@ -467,7 +474,9 @@ async def test_themes_reload_invalid( hass: HomeAssistant, themes_ws_client: MockHAClientWebSocket, invalid_theme: dict, - error: str, + error: str | None, + log: str | None, + caplog: pytest.LogCaptureFixture, ) -> None: """Test frontend.reload_themes service with an invalid theme.""" @@ -482,15 +491,24 @@ async def test_themes_reload_invalid( "homeassistant.components.frontend.async_hass_config_yaml", return_value={DOMAIN: {CONF_THEMES: invalid_theme}}, ), - pytest.raises(HomeAssistantError, match=rf"Failed to reload themes.*{error}"), + pytest.raises(HomeAssistantError, match=rf"Failed to reload themes.*{error}") + if error is not None + else nullcontext(), ): await hass.services.async_call(DOMAIN, "reload_themes", blocking=True) + if log is not None: + assert log in caplog.text + await themes_ws_client.send_json({"id": 5, "type": "frontend/get_themes"}) msg = await themes_ws_client.receive_json() - assert msg["result"]["themes"] == {"happy": {"primary-color": "pink"}} + expected_themes = {"happy": {"primary-color": "pink"}} + if error is None: + expected_themes = {} + + assert msg["result"]["themes"] == expected_themes assert msg["result"]["default_theme"] == "default"