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
parent
536d702d96
commit
35ff3afa12
|
@ -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:
|
||||
|
|
|
@ -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])
|
Loading…
Reference in New Issue