diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 294a180794..e4c0b5b84a 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1039,11 +1039,11 @@ class ArchiveRequestHandler(BaseHandler): storage_json = StorageJSON.load(storage_path) if storage_json is not None: - # Delete build folder (if exists) + # Move build folder to archive (if exists) name = storage_json.name build_folder = os.path.join(settings.config_dir, name) - if build_folder is not None: - shutil.rmtree(build_folder, os.path.join(archive_path, name)) + if os.path.exists(build_folder): + shutil.move(build_folder, os.path.join(archive_path, name)) class UnArchiveRequestHandler(BaseHandler): diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index e206090ac0..ea14309997 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -589,7 +589,7 @@ async def test_archive_request_handler_post( mock_ext_storage_path: MagicMock, tmp_path: Path, ) -> None: - """Test ArchiveRequestHandler.post method.""" + """Test ArchiveRequestHandler.post method without storage_json.""" # Set up temp directories config_dir = Path(get_fixture_path("conf")) @@ -599,14 +599,18 @@ async def test_archive_request_handler_post( 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 + # Mock storage_json to return None (no storage) + with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: + mock_load.return_value = None + + # 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() @@ -616,6 +620,112 @@ async def test_archive_request_handler_post( ).read_text() == "esphome:\n name: test_archive\n" +@pytest.mark.asyncio +async def test_archive_handler_with_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json and build folder.""" + # Set up temp directories + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + + # Create a test configuration file + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + # Create build folder with content + build_folder = config_dir / "test_device" + build_folder.mkdir() + (build_folder / "firmware.bin").write_text("binary content") + (build_folder / ".pioenvs").mkdir() + + # Mock settings to use our temp directory + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = str(test_config) + mock_archive_storage_path.return_value = str(archive_dir) + + # Mock storage_json with device name + with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_load.return_value = mock_storage + + # Archive the configuration + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + # Verify config file was moved to archive + assert not test_config.exists() + assert (archive_dir / configuration).exists() + + # Verify build folder was moved to archive + assert not build_folder.exists() + assert (archive_dir / "test_device").exists() + assert (archive_dir / "test_device" / "firmware.bin").exists() + + +@pytest.mark.asyncio +async def test_archive_handler_no_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json but no build folder.""" + # Set up temp directories + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + + # Create a test configuration file + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + # Note: No build folder created + + # Mock settings to use our temp directory + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = str(test_config) + mock_archive_storage_path.return_value = str(archive_dir) + + # Mock storage_json with device name + with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_load.return_value = mock_storage + + # Archive the configuration (should not fail even without build folder) + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + # Verify config file was moved to archive + assert not test_config.exists() + assert (archive_dir / configuration).exists() + + # Verify no build folder in archive (since it didn't exist) + assert not (archive_dir / "test_device").exists() + + @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: