core/tests/components/template/test_blueprint.py
Tudor Sandu d9b077154e
Blueprints for template entities (#126971)
* Template domain blueprints

* Default blueprint for templates

* Some linting

* Template entity updates

* Load and use blueprints in config

* Added missing mapping methods for templates

* Linting

* Added tests

* Wrong schema type

* Hassfest errors

* More linting issues

* Refactor based on desired schema

In the [architecture discussion](https://github.com/home-assistant/architecture/discussions/1027), the template blueprint instance did not specify the platform (e.g. `binary_sensor`), but the initial implementation assumed that schema.

* Create default template blueprints on first run

* Moved TemplateConfig definition

This is to avoid circular references

* Corrected methods to find templates based on blueprints

* Corrected missing entity config information

* Added tests

* Don't use hass.data

Address comments https://github.com/home-assistant/core/pull/126971/#discussion_r1780097187

* Prevent creating blueprints during testing

* Combine 2 ifs

Address comment https://github.com/home-assistant/core/pull/126971/#discussion_r1780160870

* Improve test coverage

* Prevent template component from dirtying test env

* Remove useless hard-coded validation

* Improve code coverage to 100%

* Address review comments

* Moved helpers in helpers.py

As per comment https://github.com/home-assistant/core/pull/126971#discussion_r1786539889

* Fix blueprint source URL

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-10-04 16:47:29 +02:00

243 lines
7.5 KiB
Python

"""Test blueprints."""
from collections.abc import Iterator
import contextlib
from os import PathLike
import pathlib
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components import template
from homeassistant.components.blueprint import (
BLUEPRINT_SCHEMA,
Blueprint,
BlueprintInUse,
DomainBlueprints,
)
from homeassistant.components.template import DOMAIN, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from homeassistant.util import yaml
from tests.common import async_mock_service
BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(template.__file__).parent / "blueprints"
@contextlib.contextmanager
def patch_blueprint(
blueprint_path: str, data_path: str | PathLike[str]
) -> Iterator[None]:
"""Patch blueprint loading from a different source."""
orig_load = DomainBlueprints._load_blueprint
@callback
def mock_load_blueprint(self, path):
if path != blueprint_path:
pytest.fail(f"Unexpected blueprint {path}")
return orig_load(self, path)
return Blueprint(
yaml.load_yaml(data_path),
expected_domain=self.domain,
path=path,
schema=BLUEPRINT_SCHEMA,
)
with patch(
"homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint",
mock_load_blueprint,
):
yield
@contextlib.contextmanager
def patch_invalid_blueprint() -> Iterator[None]:
"""Patch blueprint returning an invalid one."""
@callback
def mock_load_blueprint(self, path):
return Blueprint(
{
"blueprint": {
"domain": "template",
"name": "Invalid template blueprint",
},
"binary_sensor": {},
"sensor": {},
},
expected_domain=self.domain,
path=path,
schema=BLUEPRINT_SCHEMA,
)
with patch(
"homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint",
mock_load_blueprint,
):
yield
async def test_inverted_binary_sensor(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""Test inverted binary sensor blueprint."""
hass.states.async_set("binary_sensor.foo", "on", {"friendly_name": "Foo"})
hass.states.async_set("binary_sensor.bar", "off", {"friendly_name": "Bar"})
with patch_blueprint(
"inverted_binary_sensor.yaml",
BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml",
):
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"use_blueprint": {
"path": "inverted_binary_sensor.yaml",
"input": {"reference_entity": "binary_sensor.foo"},
},
"name": "Inverted foo",
},
{
"use_blueprint": {
"path": "inverted_binary_sensor.yaml",
"input": {"reference_entity": "binary_sensor.bar"},
},
"name": "Inverted bar",
},
]
},
)
hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"})
hass.states.async_set("binary_sensor.bar", "on", {"friendly_name": "Bar"})
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.foo").state == "off"
assert hass.states.get("binary_sensor.bar").state == "on"
inverted_foo = hass.states.get("binary_sensor.inverted_foo")
assert inverted_foo
assert inverted_foo.state == "on"
inverted_bar = hass.states.get("binary_sensor.inverted_bar")
assert inverted_bar
assert inverted_bar.state == "off"
foo_template = template.helpers.blueprint_in_template(hass, "binary_sensor.foo")
inverted_foo_template = template.helpers.blueprint_in_template(
hass, "binary_sensor.inverted_foo"
)
assert foo_template is None
assert inverted_foo_template == "inverted_binary_sensor.yaml"
inverted_binary_sensor_blueprint_entity_ids = (
template.helpers.templates_with_blueprint(hass, "inverted_binary_sensor.yaml")
)
assert len(inverted_binary_sensor_blueprint_entity_ids) == 2
assert len(template.helpers.templates_with_blueprint(hass, "dummy.yaml")) == 0
with pytest.raises(BlueprintInUse):
await template.async_get_blueprints(hass).async_remove_blueprint(
"inverted_binary_sensor.yaml"
)
async def test_domain_blueprint(hass: HomeAssistant) -> None:
"""Test DomainBlueprint services."""
reload_handler_calls = async_mock_service(hass, DOMAIN, SERVICE_RELOAD)
mock_create_file = MagicMock()
mock_create_file.return_value = True
with patch(
"homeassistant.components.blueprint.models.DomainBlueprints._create_file",
mock_create_file,
):
await template.async_get_blueprints(hass).async_add_blueprint(
Blueprint(
{
"blueprint": {
"domain": DOMAIN,
"name": "Test",
},
},
expected_domain="template",
path="xxx",
schema=BLUEPRINT_SCHEMA,
),
"xxx",
True,
)
assert len(reload_handler_calls) == 1
async def test_invalid_blueprint(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test an invalid blueprint definition."""
with patch_invalid_blueprint():
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"use_blueprint": {
"path": "invalid.yaml",
},
"name": "Invalid blueprint instance",
},
]
},
)
assert "more than one platform defined per blueprint" in caplog.text
assert await template.async_get_blueprints(hass).async_get_blueprints() == {}
async def test_no_blueprint(hass: HomeAssistant) -> None:
"""Test templates without blueprints."""
with patch_blueprint(
"inverted_binary_sensor.yaml",
BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml",
):
assert await async_setup_component(
hass,
"template",
{
"template": [
{"binary_sensor": {"name": "test entity", "state": "off"}},
{
"use_blueprint": {
"path": "inverted_binary_sensor.yaml",
"input": {"reference_entity": "binary_sensor.foo"},
},
"name": "inverted entity",
},
]
},
)
hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"})
await hass.async_block_till_done()
assert (
len(
template.helpers.templates_with_blueprint(
hass, "inverted_binary_sensor.yaml"
)
)
== 1
)
assert (
template.helpers.blueprint_in_template(hass, "binary_sensor.test_entity")
is None
)