mirror of
https://github.com/esphome/esphome.git
synced 2025-10-20 18:53:47 +01:00
224 lines
8.6 KiB
Python
224 lines
8.6 KiB
Python
"""Tests for dashboard settings Path-related functionality."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from argparse import Namespace
|
|
from pathlib import Path
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
from esphome.core import CORE
|
|
from esphome.dashboard.settings import DashboardSettings
|
|
|
|
|
|
@pytest.fixture
|
|
def dashboard_settings(tmp_path: Path) -> DashboardSettings:
|
|
"""Create DashboardSettings instance with temp directory."""
|
|
settings = DashboardSettings()
|
|
# Resolve symlinks to ensure paths match
|
|
resolved_dir = tmp_path.resolve()
|
|
settings.config_dir = resolved_dir
|
|
settings.absolute_config_dir = resolved_dir
|
|
return settings
|
|
|
|
|
|
def test_rel_path_simple(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path with simple relative path."""
|
|
result = dashboard_settings.rel_path("config.yaml")
|
|
|
|
expected = dashboard_settings.config_dir / "config.yaml"
|
|
assert result == expected
|
|
|
|
|
|
def test_rel_path_multiple_components(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path with multiple path components."""
|
|
result = dashboard_settings.rel_path("subfolder", "device", "config.yaml")
|
|
|
|
expected = dashboard_settings.config_dir / "subfolder" / "device" / "config.yaml"
|
|
assert result == expected
|
|
|
|
|
|
def test_rel_path_with_dots(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path prevents directory traversal."""
|
|
# This should raise ValueError as it tries to go outside config_dir
|
|
with pytest.raises(ValueError):
|
|
dashboard_settings.rel_path("..", "outside.yaml")
|
|
|
|
|
|
def test_rel_path_absolute_path_within_config(
|
|
dashboard_settings: DashboardSettings,
|
|
) -> None:
|
|
"""Test rel_path with absolute path that's within config dir."""
|
|
internal_path = dashboard_settings.absolute_config_dir / "internal.yaml"
|
|
|
|
internal_path.touch()
|
|
result = dashboard_settings.rel_path("internal.yaml")
|
|
expected = dashboard_settings.config_dir / "internal.yaml"
|
|
assert result == expected
|
|
|
|
|
|
def test_rel_path_absolute_path_outside_config(
|
|
dashboard_settings: DashboardSettings,
|
|
) -> None:
|
|
"""Test rel_path with absolute path outside config dir raises error."""
|
|
outside_path = "/tmp/outside/config.yaml"
|
|
|
|
with pytest.raises(ValueError):
|
|
dashboard_settings.rel_path(outside_path)
|
|
|
|
|
|
def test_rel_path_empty_args(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path with no arguments returns config_dir."""
|
|
result = dashboard_settings.rel_path()
|
|
assert result == dashboard_settings.config_dir
|
|
|
|
|
|
def test_rel_path_with_pathlib_path(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path works with Path objects as arguments."""
|
|
path_obj = Path("subfolder") / "config.yaml"
|
|
result = dashboard_settings.rel_path(path_obj)
|
|
|
|
expected = dashboard_settings.config_dir / "subfolder" / "config.yaml"
|
|
assert result == expected
|
|
|
|
|
|
def test_rel_path_normalizes_slashes(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path normalizes path separators."""
|
|
# os.path.join normalizes slashes on Windows but preserves them on Unix
|
|
# Test that providing components separately gives same result
|
|
result1 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml")
|
|
result2 = dashboard_settings.rel_path("folder", "subfolder", "file.yaml")
|
|
assert result1 == result2
|
|
|
|
# Also test that the result is as expected
|
|
expected = dashboard_settings.config_dir / "folder" / "subfolder" / "file.yaml"
|
|
assert result1 == expected
|
|
|
|
|
|
def test_rel_path_handles_spaces(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path handles paths with spaces."""
|
|
result = dashboard_settings.rel_path("my folder", "my config.yaml")
|
|
|
|
expected = dashboard_settings.config_dir / "my folder" / "my config.yaml"
|
|
assert result == expected
|
|
|
|
|
|
def test_rel_path_handles_special_chars(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path handles paths with special characters."""
|
|
result = dashboard_settings.rel_path("device-01_test", "config.yaml")
|
|
|
|
expected = dashboard_settings.config_dir / "device-01_test" / "config.yaml"
|
|
assert result == expected
|
|
|
|
|
|
def test_config_dir_as_path_property(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test that config_dir can be accessed and used with Path operations."""
|
|
config_path = dashboard_settings.config_dir
|
|
|
|
assert config_path.exists()
|
|
assert config_path.is_dir()
|
|
assert config_path.is_absolute()
|
|
|
|
|
|
def test_absolute_config_dir_property(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test absolute_config_dir is a Path object."""
|
|
assert isinstance(dashboard_settings.absolute_config_dir, Path)
|
|
assert dashboard_settings.absolute_config_dir.exists()
|
|
assert dashboard_settings.absolute_config_dir.is_dir()
|
|
assert dashboard_settings.absolute_config_dir.is_absolute()
|
|
|
|
|
|
def test_rel_path_symlink_inside_config(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path with symlink that points inside config dir."""
|
|
target = dashboard_settings.absolute_config_dir / "target.yaml"
|
|
target.touch()
|
|
symlink = dashboard_settings.absolute_config_dir / "link.yaml"
|
|
symlink.symlink_to(target)
|
|
result = dashboard_settings.rel_path("link.yaml")
|
|
expected = dashboard_settings.config_dir / "link.yaml"
|
|
assert result == expected
|
|
|
|
|
|
def test_rel_path_symlink_outside_config(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path with symlink that points outside config dir."""
|
|
with tempfile.NamedTemporaryFile(suffix=".yaml") as tmp:
|
|
symlink = dashboard_settings.absolute_config_dir / "external_link.yaml"
|
|
symlink.symlink_to(tmp.name)
|
|
with pytest.raises(ValueError):
|
|
dashboard_settings.rel_path("external_link.yaml")
|
|
|
|
|
|
def test_rel_path_with_none_arg(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path handles None arguments gracefully."""
|
|
result = dashboard_settings.rel_path("None")
|
|
expected = dashboard_settings.config_dir / "None"
|
|
assert result == expected
|
|
|
|
|
|
def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> None:
|
|
"""Test rel_path handles numeric arguments."""
|
|
result = dashboard_settings.rel_path("123", "456.789")
|
|
expected = dashboard_settings.config_dir / "123" / "456.789"
|
|
assert result == expected
|
|
|
|
|
|
def test_config_path_parent_resolves_to_config_dir(tmp_path: Path) -> None:
|
|
"""Test that CORE.config_path.parent resolves to config_dir after parse_args.
|
|
|
|
This is a regression test for issue #11280 where binary download failed
|
|
when using packages with secrets after the Path migration in 2025.10.0.
|
|
|
|
The issue was that after switching from os.path to Path:
|
|
- Before: os.path.dirname("/config/.") → "/config"
|
|
- After: Path("/config/.").parent → Path("/") (normalized first!)
|
|
|
|
The fix uses a sentinel file so .parent returns the correct directory:
|
|
- Fixed: Path("/config/___DASHBOARD_SENTINEL___.yaml").parent → Path("/config")
|
|
"""
|
|
# Create test directory structure with secrets and packages
|
|
config_dir = tmp_path / "config"
|
|
config_dir.mkdir()
|
|
|
|
# Create secrets.yaml with obviously fake test values
|
|
secrets_file = config_dir / "secrets.yaml"
|
|
secrets_file.write_text(
|
|
"wifi_ssid: TEST-DUMMY-SSID\n"
|
|
"wifi_password: not-a-real-password-just-for-testing\n"
|
|
)
|
|
|
|
# Create package file that uses secrets
|
|
package_file = config_dir / "common.yaml"
|
|
package_file.write_text(
|
|
"wifi:\n ssid: !secret wifi_ssid\n password: !secret wifi_password\n"
|
|
)
|
|
|
|
# Create main device config that includes the package
|
|
device_config = config_dir / "test-device.yaml"
|
|
device_config.write_text(
|
|
"esphome:\n name: test-device\n\npackages:\n common: !include common.yaml\n"
|
|
)
|
|
|
|
# Set up dashboard settings with our test config directory
|
|
settings = DashboardSettings()
|
|
args = Namespace(
|
|
configuration=str(config_dir),
|
|
password=None,
|
|
username=None,
|
|
ha_addon=False,
|
|
verbose=False,
|
|
)
|
|
settings.parse_args(args)
|
|
|
|
# Verify that CORE.config_path.parent correctly points to the config directory
|
|
# This is critical for secret resolution in yaml_util.py which does:
|
|
# main_config_dir = CORE.config_path.parent
|
|
# main_secret_yml = main_config_dir / "secrets.yaml"
|
|
assert CORE.config_path.parent == config_dir.resolve()
|
|
assert (CORE.config_path.parent / "secrets.yaml").exists()
|
|
assert (CORE.config_path.parent / "common.yaml").exists()
|
|
|
|
# Verify that CORE.config_path itself uses the sentinel file
|
|
assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml"
|
|
assert not CORE.config_path.exists() # Sentinel file doesn't actually exist
|