mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	[dashboard] Fix binary download with packages using secrets after Path migration (#11313)
This commit is contained in:
		
				
					committed by
					
						 Jesse Hills
						Jesse Hills
					
				
			
			
				
	
			
			
			
						parent
						
							ea609dc0f6
						
					
				
				
					commit
					8c1bd2fd85
				
			| @@ -2,11 +2,13 @@ | ||||
|  | ||||
| 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 | ||||
|  | ||||
|  | ||||
| @@ -159,3 +161,63 @@ def test_rel_path_with_numeric_args(dashboard_settings: DashboardSettings) -> No | ||||
|     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 | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from argparse import Namespace | ||||
| import asyncio | ||||
| from collections.abc import Generator | ||||
| from contextlib import asynccontextmanager | ||||
| @@ -17,6 +18,8 @@ from tornado.ioloop import IOLoop | ||||
| from tornado.testing import bind_unused_port | ||||
| from tornado.websocket import WebSocketClientConnection, websocket_connect | ||||
|  | ||||
| from esphome import yaml_util | ||||
| from esphome.core import CORE | ||||
| from esphome.dashboard import web_server | ||||
| from esphome.dashboard.const import DashboardEvent | ||||
| from esphome.dashboard.core import DASHBOARD | ||||
| @@ -1302,3 +1305,71 @@ async def test_dashboard_subscriber_refresh_event( | ||||
|  | ||||
|         # Give it a moment to clean up | ||||
|         await asyncio.sleep(0.01) | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_dashboard_yaml_loading_with_packages_and_secrets( | ||||
|     tmp_path: Path, | ||||
| ) -> None: | ||||
|     """Test dashboard YAML loading with packages referencing secrets. | ||||
|  | ||||
|     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. | ||||
|  | ||||
|     This test verifies that CORE.config_path initialization in the dashboard | ||||
|     allows yaml_util.load_yaml() to correctly resolve secrets from packages. | ||||
|     """ | ||||
|     # 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-download-secrets.yaml" | ||||
|     device_config.write_text( | ||||
|         "esphome:\n  name: test-download-secrets\n  platform: ESP32\n  board: esp32dev\n\n" | ||||
|         "packages:\n  common: !include common.yaml\n" | ||||
|     ) | ||||
|  | ||||
|     # Initialize DASHBOARD settings with our test config directory | ||||
|     # This is what sets CORE.config_path - the critical code path for the bug | ||||
|     args = Namespace( | ||||
|         configuration=str(config_dir), | ||||
|         password=None, | ||||
|         username=None, | ||||
|         ha_addon=False, | ||||
|         verbose=False, | ||||
|     ) | ||||
|     DASHBOARD.settings.parse_args(args) | ||||
|  | ||||
|     # With the fix: CORE.config_path should be config_dir / "___DASHBOARD_SENTINEL___.yaml" | ||||
|     # so CORE.config_path.parent would be config_dir | ||||
|     # Without the fix: CORE.config_path is config_dir / "." which normalizes to config_dir | ||||
|     # so CORE.config_path.parent would be tmp_path (the parent of config_dir) | ||||
|  | ||||
|     # The fix ensures CORE.config_path.parent points to config_dir | ||||
|     assert CORE.config_path.parent == config_dir.resolve(), ( | ||||
|         f"CORE.config_path.parent should point to config_dir. " | ||||
|         f"Got {CORE.config_path.parent}, expected {config_dir.resolve()}. " | ||||
|         f"CORE.config_path is {CORE.config_path}" | ||||
|     ) | ||||
|  | ||||
|     # Now load the YAML with packages that reference secrets | ||||
|     # This is where the bug would manifest - yaml_util.load_yaml would fail | ||||
|     # to find secrets.yaml because CORE.config_path.parent pointed to the wrong place | ||||
|     config = yaml_util.load_yaml(device_config) | ||||
|     # If we get here, secret resolution worked! | ||||
|     assert "esphome" in config | ||||
|     assert config["esphome"]["name"] == "test-download-secrets" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user