Skip to content

Commit

Permalink
Refactor unittest tests to use pytest (home-assistant#127770)
Browse files Browse the repository at this point in the history
* Refactor unittest tests to use pytest

* Add type annotations

* Use caplog to assert logs

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
  • Loading branch information
Honza-m and MartinHjelmare authored Oct 17, 2024
1 parent 536d702 commit 35ff3af
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 141 deletions.
142 changes: 1 addition & 141 deletions tests/util/yaml/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import os
import pathlib
from typing import Any
import unittest
from unittest.mock import Mock, patch

import pytest
Expand All @@ -19,7 +18,7 @@
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"])
Expand Down Expand Up @@ -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:
Expand Down
185 changes: 185 additions & 0 deletions tests/util/yaml/test_secrets.py
Original file line number Diff line number Diff line change
@@ -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])

0 comments on commit 35ff3af

Please sign in to comment.