mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00: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