diff --git a/tests/hassfest/__init__.py b/tests/hassfest/__init__.py index 1ec5a22a567..75d263fa3c8 100644 --- a/tests/hassfest/__init__.py +++ b/tests/hassfest/__init__.py @@ -1 +1,19 @@ """Tests for hassfest.""" + +from pathlib import Path + +from script.hassfest.model import Config, Integration + + +def get_integration(domain: str, config: Config): + """Helper function for creating hassfest integration model instances.""" + return Integration( + Path(domain), + _config=config, + _manifest={ + "domain": domain, + "name": domain, + "documentation": "https://example.com", + "codeowners": ["@awesome"], + }, + ) diff --git a/tests/hassfest/conftest.py b/tests/hassfest/conftest.py new file mode 100644 index 00000000000..86305b799f6 --- /dev/null +++ b/tests/hassfest/conftest.py @@ -0,0 +1,26 @@ +"""Fixtures for hassfest tests.""" + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from script.hassfest.model import Config, Integration + + +@pytest.fixture +def config(): + """Fixture for hassfest Config.""" + return Config( + root=Path(".").absolute(), + specific_integrations=None, + action="validate", + requirements=True, + ) + + +@pytest.fixture +def mock_core_integration(): + """Mock Integration to be a core one.""" + with patch.object(Integration, "core", return_value=True): + yield diff --git a/tests/hassfest/test_conditions.py b/tests/hassfest/test_conditions.py new file mode 100644 index 00000000000..09046c0007f --- /dev/null +++ b/tests/hassfest/test_conditions.py @@ -0,0 +1,154 @@ +"""Tests for hassfest conditions.""" + +import io +import json +from pathlib import Path +from unittest.mock import patch + +import pytest + +from homeassistant.util.yaml.loader import parse_yaml +from script.hassfest import conditions +from script.hassfest.model import Config + +from . import get_integration + +CONDITION_DESCRIPTION_FILENAME = "conditions.yaml" +CONDITION_ICONS_FILENAME = "icons.json" +CONDITION_STRINGS_FILENAME = "strings.json" + +CONDITION_DESCRIPTIONS = { + "valid": { + CONDITION_DESCRIPTION_FILENAME: """ + _: + fields: + after: + example: sunrise + selector: + select: + options: + - sunrise + - sunset + after_offset: + selector: + time: null + """, + CONDITION_ICONS_FILENAME: {"conditions": {"_": {"condition": "mdi:flash"}}}, + CONDITION_STRINGS_FILENAME: { + "conditions": { + "_": { + "name": "Sun", + "description": "When the sun is above/below the horizon", + "description_configured": "When a the sun rises or sets.", + "fields": { + "after": {"name": "After event", "description": "The event."}, + "after_offset": { + "name": "Offset", + "description": "The offset.", + }, + }, + } + } + }, + "errors": [], + }, + "yaml_missing_colon": { + CONDITION_DESCRIPTION_FILENAME: """ + test: + fields + entity: + selector: + entity: + """, + "errors": ["Invalid conditions.yaml"], + }, + "invalid_conditions_schema": { + CONDITION_DESCRIPTION_FILENAME: """ + invalid_condition: + fields: + entity: + selector: + invalid_selector: null + """, + "errors": ["Unknown selector type invalid_selector"], + }, + "missing_strings_and_icons": { + CONDITION_DESCRIPTION_FILENAME: """ + sun: + fields: + after: + example: sunrise + selector: + select: + options: + - sunrise + - sunset + translation_key: after + after_offset: + selector: + time: null + """, + CONDITION_ICONS_FILENAME: {"conditions": {}}, + CONDITION_STRINGS_FILENAME: { + "conditions": { + "sun": { + "fields": { + "after_offset": {}, + }, + } + } + }, + "errors": [ + "has no icon", + "has no name", + "has no description", + "field after with no name", + "field after with no description", + "field after with a selector with a translation key", + "field after_offset with no name", + "field after_offset with no description", + ], + }, +} + + +@pytest.mark.usefixtures("mock_core_integration") +def test_validate(config: Config) -> None: + """Test validate version with no key.""" + + def _load_yaml(fname, secrets=None): + domain, yaml_file = fname.split("/") + assert yaml_file == CONDITION_DESCRIPTION_FILENAME + + condition_descriptions = CONDITION_DESCRIPTIONS[domain][yaml_file] + with io.StringIO(condition_descriptions) as file: + return parse_yaml(file) + + def _patched_path_read_text(path: Path): + domain = path.parent.name + filename = path.name + + return json.dumps(CONDITION_DESCRIPTIONS[domain][filename]) + + integrations = { + domain: get_integration(domain, config) for domain in CONDITION_DESCRIPTIONS + } + + with ( + patch("script.hassfest.conditions.grep_dir", return_value=True), + patch("pathlib.Path.is_file", return_value=True), + patch("pathlib.Path.read_text", _patched_path_read_text), + patch("annotatedyaml.loader.load_yaml", side_effect=_load_yaml), + ): + conditions.validate(integrations, config) + + assert not config.errors + + for domain, description in CONDITION_DESCRIPTIONS.items(): + assert len(integrations[domain].errors) == len(description["errors"]), ( + f"Domain '{domain}' has unexpected errors: {integrations[domain].errors}" + ) + for error, expected_error in zip( + integrations[domain].errors, description["errors"], strict=True + ): + assert expected_error in error.error diff --git a/tests/hassfest/test_triggers.py b/tests/hassfest/test_triggers.py index 236e6f96134..9cf327a0e0e 100644 --- a/tests/hassfest/test_triggers.py +++ b/tests/hassfest/test_triggers.py @@ -9,7 +9,9 @@ import pytest from homeassistant.util.yaml.loader import parse_yaml from script.hassfest import triggers -from script.hassfest.model import Config, Integration +from script.hassfest.model import Config + +from . import get_integration TRIGGER_DESCRIPTION_FILENAME = "triggers.yaml" TRIGGER_ICONS_FILENAME = "icons.json" @@ -107,38 +109,6 @@ TRIGGER_DESCRIPTIONS = { } -@pytest.fixture -def config(): - """Fixture for hassfest Config.""" - return Config( - root=Path(".").absolute(), - specific_integrations=None, - action="validate", - requirements=True, - ) - - -@pytest.fixture -def mock_core_integration(): - """Mock Integration to be a core one.""" - with patch.object(Integration, "core", return_value=True): - yield - - -def get_integration(domain: str, config: Config): - """Fixture for hassfest integration model.""" - return Integration( - Path(domain), - _config=config, - _manifest={ - "domain": domain, - "name": domain, - "documentation": "https://example.com", - "codeowners": ["@awesome"], - }, - ) - - @pytest.mark.usefixtures("mock_core_integration") def test_validate(config: Config) -> None: """Test validate version with no key."""