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>
pull/128922/head
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 pathlib
from typing import Any
import unittest
from unittest.mock import Mock, patch
import pytest
@ -19,7 +18,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import yaml
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"])
@ -396,145 +395,6 @@ def test_dump_unicode() -> None:
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.usefixtures("try_both_dumpers", "mock_hass_config_yaml")
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])