mirror of
https://github.com/esphome/esphome.git
synced 2025-09-14 17:22:20 +01:00
Add additional dashboard and main tests (#10688)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -556,6 +556,66 @@ def test_start_web_server_with_address_port(
|
|||||||
assert (archive_dir / "old.yaml").exists()
|
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.skipif(os.name == "nt", reason="Unix sockets are not supported on Windows")
|
||||||
@pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path")
|
@pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path")
|
||||||
def test_start_web_server_with_unix_socket(tmp_path: Path) -> None:
|
def test_start_web_server_with_unix_socket(tmp_path: Path) -> None:
|
||||||
|
@@ -9,18 +9,27 @@ from typing import Any
|
|||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
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 (
|
from esphome.const import (
|
||||||
CONF_BROKER,
|
CONF_BROKER,
|
||||||
CONF_DISABLED,
|
CONF_DISABLED,
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
CONF_MDNS,
|
CONF_MDNS,
|
||||||
CONF_MQTT,
|
CONF_MQTT,
|
||||||
|
CONF_NAME,
|
||||||
CONF_OTA,
|
CONF_OTA,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_SUBSTITUTIONS,
|
||||||
CONF_USE_ADDRESS,
|
CONF_USE_ADDRESS,
|
||||||
CONF_WIFI,
|
CONF_WIFI,
|
||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
@@ -170,6 +179,14 @@ def mock_has_mqtt_logging() -> Generator[Mock]:
|
|||||||
yield 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:
|
def test_choose_upload_log_host_with_string_default() -> None:
|
||||||
"""Test with a single string default device."""
|
"""Test with a single string default device."""
|
||||||
result = choose_upload_log_host(
|
result = choose_upload_log_host(
|
||||||
@@ -606,6 +623,9 @@ class MockArgs:
|
|||||||
password: str | None = None
|
password: str | None = None
|
||||||
client_id: str | None = None
|
client_id: str | None = None
|
||||||
topic: str | None = None
|
topic: str | None = None
|
||||||
|
configuration: str | None = None
|
||||||
|
name: str | None = None
|
||||||
|
dashboard: bool = False
|
||||||
|
|
||||||
|
|
||||||
def test_upload_program_serial_esp32(
|
def test_upload_program_serial_esp32(
|
||||||
@@ -1053,3 +1073,178 @@ def test_show_logs_platform_specific_handler(
|
|||||||
assert result == 0
|
assert result == 0
|
||||||
mock_import.assert_called_once_with("esphome.components.custom_platform")
|
mock_import.assert_called_once_with("esphome.components.custom_platform")
|
||||||
mock_module.show_logs.assert_called_once_with(config, args, devices)
|
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
|
||||||
|
Reference in New Issue
Block a user