"""Test blueprint models.""" import logging from unittest.mock import patch import pytest from homeassistant.components.blueprint import errors, models from homeassistant.util.yaml import Input @pytest.fixture def blueprint_1(): """Blueprint fixture.""" return models.Blueprint( { "blueprint": { "name": "Hello", "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "input": {"test-input": {"name": "Name", "description": "Description"}}, }, "example": Input("test-input"), } ) @pytest.fixture def blueprint_2(): """Blueprint fixture with default inputs.""" return models.Blueprint( { "blueprint": { "name": "Hello", "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "input": { "test-input": {"name": "Name", "description": "Description"}, "test-input-default": {"default": "test"}, }, }, "example": Input("test-input"), "example-default": Input("test-input-default"), } ) @pytest.fixture def domain_bps(hass): """Domain blueprints fixture.""" return models.DomainBlueprints(hass, "automation", logging.getLogger(__name__)) def test_blueprint_model_init(): """Test constructor validation.""" with pytest.raises(errors.InvalidBlueprint): models.Blueprint({}) with pytest.raises(errors.InvalidBlueprint): models.Blueprint( {"blueprint": {"name": "Hello", "domain": "automation"}}, expected_domain="not-automation", ) with pytest.raises(errors.InvalidBlueprint): models.Blueprint( { "blueprint": { "name": "Hello", "domain": "automation", "input": {"something": None}, }, "trigger": {"platform": Input("non-existing")}, } ) def test_blueprint_properties(blueprint_1): """Test properties.""" assert blueprint_1.metadata == { "name": "Hello", "domain": "automation", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", "input": {"test-input": {"name": "Name", "description": "Description"}}, } assert blueprint_1.domain == "automation" assert blueprint_1.name == "Hello" assert blueprint_1.inputs == { "test-input": {"name": "Name", "description": "Description"} } def test_blueprint_update_metadata(): """Test update metadata.""" bp = models.Blueprint( { "blueprint": { "name": "Hello", "domain": "automation", }, } ) bp.update_metadata(source_url="http://bla.com") assert bp.metadata["source_url"] == "http://bla.com" def test_blueprint_validate(): """Test validate blueprint.""" assert ( models.Blueprint( { "blueprint": { "name": "Hello", "domain": "automation", }, } ).validate() is None ) assert ( models.Blueprint( { "blueprint": { "name": "Hello", "domain": "automation", "homeassistant": {"min_version": "100000.0.0"}, }, } ).validate() == ["Requires at least Home Assistant 100000.0.0"] ) def test_blueprint_inputs(blueprint_2): """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, { "use_blueprint": { "path": "bla", "input": {"test-input": 1, "test-input-default": 12}, }, "example-default": {"overridden": "via-config"}, }, ) inputs.validate() assert inputs.inputs == {"test-input": 1, "test-input-default": 12} assert inputs.async_substitute() == { "example": 1, "example-default": {"overridden": "via-config"}, } def test_blueprint_inputs_validation(blueprint_1): """Test blueprint input validation.""" inputs = models.BlueprintInputs( blueprint_1, {"use_blueprint": {"path": "bla", "input": {"non-existing-placeholder": 1}}}, ) with pytest.raises(errors.MissingInput): inputs.validate() def test_blueprint_inputs_default(blueprint_2): """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, {"use_blueprint": {"path": "bla", "input": {"test-input": 1}}}, ) inputs.validate() assert inputs.inputs == {"test-input": 1} assert inputs.inputs_with_default == { "test-input": 1, "test-input-default": "test", } assert inputs.async_substitute() == {"example": 1, "example-default": "test"} def test_blueprint_inputs_override_default(blueprint_2): """Test blueprint inputs.""" inputs = models.BlueprintInputs( blueprint_2, { "use_blueprint": { "path": "bla", "input": {"test-input": 1, "test-input-default": "custom"}, } }, ) inputs.validate() assert inputs.inputs == { "test-input": 1, "test-input-default": "custom", } assert inputs.inputs_with_default == { "test-input": 1, "test-input-default": "custom", } assert inputs.async_substitute() == {"example": 1, "example-default": "custom"} async def test_domain_blueprints_get_blueprint_errors(hass, domain_bps): """Test domain blueprints.""" assert hass.data["blueprint"]["automation"] is domain_bps with pytest.raises(errors.FailedToLoad), patch( "homeassistant.util.yaml.load_yaml", side_effect=FileNotFoundError ): await domain_bps.async_get_blueprint("non-existing-path") with patch( "homeassistant.util.yaml.load_yaml", return_value={"blueprint": "invalid"} ), pytest.raises(errors.FailedToLoad): await domain_bps.async_get_blueprint("non-existing-path") async def test_domain_blueprints_caching(domain_bps): """Test domain blueprints cache blueprints.""" obj = object() with patch.object(domain_bps, "_load_blueprint", return_value=obj): assert await domain_bps.async_get_blueprint("something") is obj # Now we hit cache assert await domain_bps.async_get_blueprint("something") is obj obj_2 = object() domain_bps.async_reset_cache() # Now we call this method again. with patch.object(domain_bps, "_load_blueprint", return_value=obj_2): assert await domain_bps.async_get_blueprint("something") is obj_2 async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1): """Test DomainBlueprints.async_inputs_from_config.""" with pytest.raises(errors.InvalidBlueprintInputs): await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"}) with pytest.raises(errors.MissingInput), patch.object( domain_bps, "async_get_blueprint", return_value=blueprint_1 ): await domain_bps.async_inputs_from_config( {"use_blueprint": {"path": "bla.yaml", "input": {}}} ) with patch.object(domain_bps, "async_get_blueprint", return_value=blueprint_1): inputs = await domain_bps.async_inputs_from_config( {"use_blueprint": {"path": "bla.yaml", "input": {"test-input": None}}} ) assert inputs.blueprint is blueprint_1 assert inputs.inputs == {"test-input": None} async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1): """Test DomainBlueprints.async_add_blueprint.""" with patch.object(domain_bps, "_create_file") as create_file_mock: # Should add extension when not present. await domain_bps.async_add_blueprint(blueprint_1, "something") assert create_file_mock.call_args[0][1] == ("something.yaml") await domain_bps.async_add_blueprint(blueprint_1, "something2.yaml") assert create_file_mock.call_args[0][1] == ("something2.yaml") # Should be in cache. with patch.object(domain_bps, "_load_blueprint") as mock_load: assert await domain_bps.async_get_blueprint("something.yaml") == blueprint_1 assert not mock_load.mock_calls async def test_inputs_from_config_nonexisting_blueprint(domain_bps): """Test referring non-existing blueprint.""" with pytest.raises(errors.FailedToLoad): await domain_bps.async_inputs_from_config( {"use_blueprint": {"path": "non-existing.yaml"}} )