Refactor unittest tests to use pytest (#127770)

* Refactor unittest tests to use pytest

* Add type annotations

* Use caplog to assert logs

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Jan Morawiec 2024-10-17 20:28:14 +01:00 committed by GitHub
parent 536d702d96
commit 35ff3afa12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 186 additions and 141 deletions

View File

@ -6,7 +6,6 @@ import io
import os import os
import pathlib import pathlib
from typing import Any from typing import Any
import unittest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
@ -19,7 +18,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import yaml from homeassistant.util import yaml
from homeassistant.util.yaml import loader as yaml_loader from homeassistant.util.yaml import loader as yaml_loader
from tests.common import extract_stack_to_frame, get_test_config_dir, patch_yaml_files from tests.common import extract_stack_to_frame
@pytest.fixture(params=["enable_c_loader", "disable_c_loader"]) @pytest.fixture(params=["enable_c_loader", "disable_c_loader"])
@ -396,145 +395,6 @@ def test_dump_unicode() -> None:
assert yaml.dump({"a": None, "b": "привет"}) == "a:\nb: привет\n" assert yaml.dump({"a": None, "b": "привет"}) == "a:\nb: привет\n"
FILES = {}
def load_yaml(fname, string, secrets=None):
"""Write a string to file and return the parsed yaml."""
FILES[fname] = string
with patch_yaml_files(FILES):
return load_yaml_config_file(fname, secrets)
class TestSecrets(unittest.TestCase):
"""Test the secrets parameter in the yaml utility."""
def setUp(self):
"""Create & load secrets file."""
config_dir = get_test_config_dir()
self._yaml_path = os.path.join(config_dir, YAML_CONFIG_FILE)
self._secret_path = os.path.join(config_dir, yaml.SECRET_YAML)
self._sub_folder_path = os.path.join(config_dir, "subFolder")
self._unrelated_path = os.path.join(config_dir, "unrelated")
load_yaml(
self._secret_path,
(
"http_pw: pwhttp\n"
"comp1_un: un1\n"
"comp1_pw: pw1\n"
"stale_pw: not_used\n"
"logger: debug\n"
),
)
self._yaml = load_yaml(
self._yaml_path,
(
"http:\n"
" api_password: !secret http_pw\n"
"component:\n"
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
""
),
yaml_loader.Secrets(config_dir),
)
def tearDown(self):
"""Clean up secrets."""
FILES.clear()
def test_secrets_from_yaml(self):
"""Did secrets load ok."""
expected = {"api_password": "pwhttp"}
assert expected == self._yaml["http"]
expected = {"username": "un1", "password": "pw1"}
assert expected == self._yaml["component"]
def test_secrets_from_parent_folder(self):
"""Test loading secrets from parent folder."""
expected = {"api_password": "pwhttp"}
self._yaml = load_yaml(
os.path.join(self._sub_folder_path, "sub.yaml"),
(
"http:\n"
" api_password: !secret http_pw\n"
"component:\n"
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
""
),
yaml_loader.Secrets(get_test_config_dir()),
)
assert expected == self._yaml["http"]
def test_secret_overrides_parent(self):
"""Test loading current directory secret overrides the parent."""
expected = {"api_password": "override"}
load_yaml(
os.path.join(self._sub_folder_path, yaml.SECRET_YAML), "http_pw: override"
)
self._yaml = load_yaml(
os.path.join(self._sub_folder_path, "sub.yaml"),
(
"http:\n"
" api_password: !secret http_pw\n"
"component:\n"
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
""
),
yaml_loader.Secrets(get_test_config_dir()),
)
assert expected == self._yaml["http"]
def test_secrets_from_unrelated_fails(self):
"""Test loading secrets from unrelated folder fails."""
load_yaml(os.path.join(self._unrelated_path, yaml.SECRET_YAML), "test: failure")
with pytest.raises(HomeAssistantError):
load_yaml(
os.path.join(self._sub_folder_path, "sub.yaml"),
"http:\n api_password: !secret test",
)
def test_secrets_logger_removed(self):
"""Ensure logger: debug was removed."""
with pytest.raises(HomeAssistantError):
load_yaml(self._yaml_path, "api_password: !secret logger")
@patch("homeassistant.util.yaml.loader._LOGGER.error")
def test_bad_logger_value(self, mock_error):
"""Ensure logger: debug was removed."""
load_yaml(self._secret_path, "logger: info\npw: abc")
load_yaml(
self._yaml_path,
"api_password: !secret pw",
yaml_loader.Secrets(get_test_config_dir()),
)
assert mock_error.call_count == 1, "Expected an error about logger: value"
def test_secrets_are_not_dict(self):
"""Did secrets handle non-dict file."""
FILES[self._secret_path] = (
"- http_pw: pwhttp\n comp1_un: un1\n comp1_pw: pw1\n"
)
with pytest.raises(HomeAssistantError):
load_yaml(
self._yaml_path,
(
"http:\n"
" api_password: !secret http_pw\n"
"component:\n"
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
""
),
)
@pytest.mark.parametrize("hass_config_yaml", ['key: [1, "2", 3]']) @pytest.mark.parametrize("hass_config_yaml", ['key: [1, "2", 3]'])
@pytest.mark.usefixtures("try_both_dumpers", "mock_hass_config_yaml") @pytest.mark.usefixtures("try_both_dumpers", "mock_hass_config_yaml")
def test_representing_yaml_loaded_data() -> None: def test_representing_yaml_loaded_data() -> None:

View File

@ -0,0 +1,185 @@
"""Test Home Assistant secret substitution in YAML files."""
from dataclasses import dataclass
import logging
from pathlib import Path
import pytest
from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import yaml
from homeassistant.util.yaml import loader as yaml_loader
from tests.common import get_test_config_dir, patch_yaml_files
@dataclass(frozen=True)
class YamlFile:
"""Represents a .yaml file used for testing."""
path: Path
contents: str
def load_config_file(config_file_path: Path, files: list[YamlFile]):
"""Patch secret files and return the loaded config file."""
patch_files = {x.path.as_posix(): x.contents for x in files}
with patch_yaml_files(patch_files):
return load_yaml_config_file(
config_file_path.as_posix(),
yaml_loader.Secrets(Path(get_test_config_dir())),
)
@pytest.fixture
def filepaths() -> dict[str, Path]:
"""Return a dictionary of filepaths for testing."""
config_dir = Path(get_test_config_dir())
return {
"config": config_dir,
"sub_folder": config_dir / "subFolder",
"unrelated": config_dir / "unrelated",
}
@pytest.fixture
def default_config(filepaths: dict[str, Path]) -> YamlFile:
"""Return the default config file for testing."""
return YamlFile(
path=filepaths["config"] / YAML_CONFIG_FILE,
contents=(
"http:\n"
" api_password: !secret http_pw\n"
"component:\n"
" username: !secret comp1_un\n"
" password: !secret comp1_pw\n"
""
),
)
@pytest.fixture
def default_secrets(filepaths: dict[str, Path]) -> YamlFile:
"""Return the default secrets file for testing."""
return YamlFile(
path=filepaths["config"] / yaml.SECRET_YAML,
contents=(
"http_pw: pwhttp\n"
"comp1_un: un1\n"
"comp1_pw: pw1\n"
"stale_pw: not_used\n"
"logger: debug\n"
),
)
def test_secrets_from_yaml(default_config: YamlFile, default_secrets: YamlFile) -> None:
"""Did secrets load ok."""
loaded_file = load_config_file(
default_config.path, [default_config, default_secrets]
)
expected = {"api_password": "pwhttp"}
assert expected == loaded_file["http"]
expected = {"username": "un1", "password": "pw1"}
assert expected == loaded_file["component"]
def test_secrets_from_parent_folder(
filepaths: dict[str, Path],
default_config: YamlFile,
default_secrets: YamlFile,
) -> None:
"""Test loading secrets from parent folder."""
config_file = YamlFile(
path=filepaths["sub_folder"] / "sub.yaml",
contents=default_config.contents,
)
loaded_file = load_config_file(config_file.path, [config_file, default_secrets])
expected = {"api_password": "pwhttp"}
assert expected == loaded_file["http"]
def test_secret_overrides_parent(
filepaths: dict[str, Path],
default_config: YamlFile,
default_secrets: YamlFile,
) -> None:
"""Test loading current directory secret overrides the parent."""
config_file = YamlFile(
path=filepaths["sub_folder"] / "sub.yaml", contents=default_config.contents
)
sub_secrets = YamlFile(
path=filepaths["sub_folder"] / yaml.SECRET_YAML, contents="http_pw: override"
)
loaded_file = load_config_file(
config_file.path, [config_file, default_secrets, sub_secrets]
)
expected = {"api_password": "override"}
assert loaded_file["http"] == expected
def test_secrets_from_unrelated_fails(
filepaths: dict[str, Path],
default_secrets: YamlFile,
) -> None:
"""Test loading secrets from unrelated folder fails."""
config_file = YamlFile(
path=filepaths["sub_folder"] / "sub.yaml",
contents="http:\n api_password: !secret test",
)
unrelated_secrets = YamlFile(
path=filepaths["unrelated"] / yaml.SECRET_YAML, contents="test: failure"
)
with pytest.raises(HomeAssistantError, match="Secret test not defined"):
load_config_file(
config_file.path, [config_file, default_secrets, unrelated_secrets]
)
def test_secrets_logger_removed(
filepaths: dict[str, Path],
default_secrets: YamlFile,
) -> None:
"""Ensure logger: debug gets removed from secrets file once logger is configured."""
config_file = YamlFile(
path=filepaths["config"] / YAML_CONFIG_FILE,
contents="api_password: !secret logger",
)
with pytest.raises(HomeAssistantError, match="Secret logger not defined"):
load_config_file(config_file.path, [config_file, default_secrets])
def test_bad_logger_value(
caplog: pytest.LogCaptureFixture, filepaths: dict[str, Path]
) -> None:
"""Ensure only logger: debug is allowed in secret file."""
config_file = YamlFile(
path=filepaths["config"] / YAML_CONFIG_FILE, contents="api_password: !secret pw"
)
secrets_file = YamlFile(
path=filepaths["config"] / yaml.SECRET_YAML, contents="logger: info\npw: abc"
)
with caplog.at_level(logging.ERROR):
load_config_file(config_file.path, [config_file, secrets_file])
assert (
"Error in secrets.yaml: 'logger: debug' expected, but 'logger: info' found"
in caplog.messages
)
def test_secrets_are_not_dict(
filepaths: dict[str, Path],
default_config: YamlFile,
) -> None:
"""Did secrets handle non-dict file."""
non_dict_secrets = YamlFile(
path=filepaths["config"] / yaml.SECRET_YAML,
contents="- http_pw: pwhttp\n comp1_un: un1\n comp1_pw: pw1\n",
)
with pytest.raises(HomeAssistantError, match="Secrets is not a dictionary"):
load_config_file(default_config.path, [default_config, non_dict_secrets])