1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-14 09:12:19 +01:00

Add additional dashboard and main tests (#10688)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2025-09-12 16:04:56 -05:00
committed by GitHub
parent cf1fef8cfb
commit 24eb33a1c0
2 changed files with 256 additions and 1 deletions

View File

@@ -556,6 +556,66 @@ def test_start_web_server_with_address_port(
assert (archive_dir / "old.yaml").exists()
@pytest.mark.asyncio
async def test_edit_request_handler_get(dashboard: DashboardTestHelper) -> None:
"""Test EditRequestHandler.get method."""
# Test getting a valid yaml file
response = await dashboard.fetch("/edit?configuration=pico.yaml")
assert response.code == 200
assert response.headers["content-type"] == "application/yaml"
content = response.body.decode()
assert "esphome:" in content # Verify it's a valid ESPHome config
# Test getting a non-existent file
with pytest.raises(HTTPClientError) as exc_info:
await dashboard.fetch("/edit?configuration=nonexistent.yaml")
assert exc_info.value.code == 404
# Test getting a non-yaml file
with pytest.raises(HTTPClientError) as exc_info:
await dashboard.fetch("/edit?configuration=test.txt")
assert exc_info.value.code == 404
# Test path traversal attempt
with pytest.raises(HTTPClientError) as exc_info:
await dashboard.fetch("/edit?configuration=../../../etc/passwd")
assert exc_info.value.code == 404
@pytest.mark.asyncio
async def test_archive_request_handler_post(
dashboard: DashboardTestHelper,
mock_archive_storage_path: MagicMock,
mock_ext_storage_path: MagicMock,
tmp_path: Path,
) -> None:
"""Test ArchiveRequestHandler.post method."""
# Set up temp directories
config_dir = Path(get_fixture_path("conf"))
archive_dir = tmp_path / "archive"
# Create a test configuration file
test_config = config_dir / "test_archive.yaml"
test_config.write_text("esphome:\n name: test_archive\n")
# Archive the configuration
response = await dashboard.fetch(
"/archive",
method="POST",
body="configuration=test_archive.yaml",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
assert response.code == 200
# Verify file was moved to archive
assert not test_config.exists()
assert (archive_dir / "test_archive.yaml").exists()
assert (
archive_dir / "test_archive.yaml"
).read_text() == "esphome:\n name: test_archive\n"
@pytest.mark.skipif(os.name == "nt", reason="Unix sockets are not supported on Windows")
@pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path")
def test_start_web_server_with_unix_socket(tmp_path: Path) -> None:

View File

@@ -9,18 +9,27 @@ from typing import Any
from unittest.mock import MagicMock, Mock, patch
import pytest
from pytest import CaptureFixture
from esphome.__main__ import choose_upload_log_host, show_logs, upload_program
from esphome.__main__ import (
choose_upload_log_host,
command_rename,
command_wizard,
show_logs,
upload_program,
)
from esphome.const import (
CONF_BROKER,
CONF_DISABLED,
CONF_ESPHOME,
CONF_MDNS,
CONF_MQTT,
CONF_NAME,
CONF_OTA,
CONF_PASSWORD,
CONF_PLATFORM,
CONF_PORT,
CONF_SUBSTITUTIONS,
CONF_USE_ADDRESS,
CONF_WIFI,
KEY_CORE,
@@ -170,6 +179,14 @@ def mock_has_mqtt_logging() -> Generator[Mock]:
yield mock
@pytest.fixture
def mock_run_external_process() -> Generator[Mock]:
"""Mock run_external_process for testing."""
with patch("esphome.__main__.run_external_process") as mock:
mock.return_value = 0 # Default to success
yield mock
def test_choose_upload_log_host_with_string_default() -> None:
"""Test with a single string default device."""
result = choose_upload_log_host(
@@ -606,6 +623,9 @@ class MockArgs:
password: str | None = None
client_id: str | None = None
topic: str | None = None
configuration: str | None = None
name: str | None = None
dashboard: bool = False
def test_upload_program_serial_esp32(
@@ -1053,3 +1073,178 @@ def test_show_logs_platform_specific_handler(
assert result == 0
mock_import.assert_called_once_with("esphome.components.custom_platform")
mock_module.show_logs.assert_called_once_with(config, args, devices)
def test_command_wizard(tmp_path: Path) -> None:
"""Test command_wizard function."""
config_file = tmp_path / "test.yaml"
# Mock wizard.wizard to avoid interactive prompts
with patch("esphome.wizard.wizard") as mock_wizard:
mock_wizard.return_value = 0
args = MockArgs(configuration=str(config_file))
result = command_wizard(args)
assert result == 0
mock_wizard.assert_called_once_with(str(config_file))
def test_command_rename_invalid_characters(
tmp_path: Path, capfd: CaptureFixture[str]
) -> None:
"""Test command_rename with invalid characters in name."""
setup_core(tmp_path=tmp_path)
# Test with invalid character (space)
args = MockArgs(name="invalid name")
result = command_rename(args, {})
assert result == 1
captured = capfd.readouterr()
assert "invalid character" in captured.out.lower()
def test_command_rename_complex_yaml(
tmp_path: Path, capfd: CaptureFixture[str]
) -> None:
"""Test command_rename with complex YAML that cannot be renamed."""
config_file = tmp_path / "test.yaml"
config_file.write_text("# Complex YAML without esphome section\nsome_key: value\n")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
args = MockArgs(name="newname")
result = command_rename(args, {})
assert result == 1
captured = capfd.readouterr()
assert "complex yaml" in captured.out.lower()
def test_command_rename_success(
tmp_path: Path,
capfd: CaptureFixture[str],
mock_run_external_process: Mock,
) -> None:
"""Test successful rename of a simple configuration."""
config_file = tmp_path / "oldname.yaml"
config_file.write_text("""
esphome:
name: oldname
esp32:
board: nodemcu-32s
wifi:
ssid: "test"
password: "test1234"
""")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
# Set up CORE.config to avoid ValueError when accessing CORE.address
CORE.config = {CONF_ESPHOME: {CONF_NAME: "oldname"}}
args = MockArgs(name="newname", dashboard=False)
# Simulate successful validation and upload
mock_run_external_process.return_value = 0
result = command_rename(args, {})
assert result == 0
# Verify new file was created
new_file = tmp_path / "newname.yaml"
assert new_file.exists()
# Verify old file was removed
assert not config_file.exists()
# Verify content was updated
content = new_file.read_text()
assert (
'name: "newname"' in content
or "name: 'newname'" in content
or "name: newname" in content
)
captured = capfd.readouterr()
assert "SUCCESS" in captured.out
def test_command_rename_with_substitutions(
tmp_path: Path,
mock_run_external_process: Mock,
) -> None:
"""Test rename with substitutions in YAML."""
config_file = tmp_path / "oldname.yaml"
config_file.write_text("""
substitutions:
device_name: oldname
esphome:
name: ${device_name}
esp32:
board: nodemcu-32s
""")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
# Set up CORE.config to avoid ValueError when accessing CORE.address
CORE.config = {
CONF_ESPHOME: {CONF_NAME: "oldname"},
CONF_SUBSTITUTIONS: {"device_name": "oldname"},
}
args = MockArgs(name="newname", dashboard=False)
mock_run_external_process.return_value = 0
result = command_rename(args, {})
assert result == 0
# Verify substitution was updated
new_file = tmp_path / "newname.yaml"
content = new_file.read_text()
assert 'device_name: "newname"' in content
def test_command_rename_validation_failure(
tmp_path: Path,
capfd: CaptureFixture[str],
mock_run_external_process: Mock,
) -> None:
"""Test rename when validation fails."""
config_file = tmp_path / "oldname.yaml"
config_file.write_text("""
esphome:
name: oldname
esp32:
board: nodemcu-32s
""")
setup_core(tmp_path=tmp_path)
CORE.config_path = str(config_file)
args = MockArgs(name="newname", dashboard=False)
# First call for validation fails
mock_run_external_process.return_value = 1
result = command_rename(args, {})
assert result == 1
# Verify new file was created but then removed due to failure
new_file = tmp_path / "newname.yaml"
assert not new_file.exists()
# Verify old file still exists (not removed on failure)
assert config_file.exists()
captured = capfd.readouterr()
assert "Rename failed" in captured.out