From fb3105bdc004ec8283fd61966c68509b1ade596c Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 2 Jan 2025 11:37:25 +0100 Subject: [PATCH] Improve Supervisor backup error handling (#134346) * Raise Home Assistant error in case backup restore fails This change raises a Home Assistant error in case the backup restore fails. The Supervisor is checking some common issues before starting the actual restore in background. This early checks raise an exception (represented by a HTTP 400 error). This change catches such errors and raises a Home Assistant error with the message from the Supervisor exception. * Add test coverage --- homeassistant/components/hassio/backup.py | 32 ++++++---- tests/components/hassio/test_backup.py | 71 +++++++++++++++++++++++ 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hassio/backup.py b/homeassistant/components/hassio/backup.py index 1b7cf930588..ad3bd78adec 100644 --- a/homeassistant/components/hassio/backup.py +++ b/homeassistant/components/hassio/backup.py @@ -28,6 +28,9 @@ from homeassistant.components.backup import ( NewBackup, WrittenBackup, ) + +# pylint: disable-next=hass-component-root-import +from homeassistant.components.backup.manager import IncorrectPasswordError from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -385,17 +388,24 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): agent = cast(SupervisorBackupAgent, manager.backup_agents[agent_id]) restore_location = agent.location - job = await self._client.backups.partial_restore( - backup_id, - supervisor_backups.PartialRestoreOptions( - addons=restore_addons_set, - folders=restore_folders_set, - homeassistant=restore_homeassistant, - password=password, - background=True, - location=restore_location, - ), - ) + try: + job = await self._client.backups.partial_restore( + backup_id, + supervisor_backups.PartialRestoreOptions( + addons=restore_addons_set, + folders=restore_folders_set, + homeassistant=restore_homeassistant, + password=password, + background=True, + location=restore_location, + ), + ) + except SupervisorBadRequestError as err: + # Supervisor currently does not transmit machine parsable error types + message = err.args[0] + if message.startswith("Invalid password for backup"): + raise IncorrectPasswordError(message) from err + raise HomeAssistantError(message) from err restore_complete = asyncio.Event() diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index 3c9440c41ff..450c9ddded9 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -997,6 +997,77 @@ async def test_reader_writer_restore( assert response["result"] is None +@pytest.mark.parametrize( + ("supervisor_error_string", "expected_error_code"), + [ + ( + "Invalid password for backup", + "password_incorrect", + ), + ( + "Backup was made on supervisor version 2025.12.0, can't restore on 2024.12.0. Must update supervisor first.", + "home_assistant_error", + ), + ], +) +@pytest.mark.usefixtures("hassio_client", "setup_integration") +async def test_reader_writer_restore_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + supervisor_client: AsyncMock, + supervisor_error_string: str, + expected_error_code: str, +) -> None: + """Test restoring a backup.""" + client = await hass_ws_client(hass) + supervisor_client.backups.partial_restore.side_effect = SupervisorBadRequestError( + supervisor_error_string + ) + supervisor_client.backups.list.return_value = [TEST_BACKUP] + supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS + + await client.send_json_auto_id({"type": "backup/subscribe_events"}) + response = await client.receive_json() + assert response["event"] == {"manager_state": "idle"} + response = await client.receive_json() + assert response["success"] + + await client.send_json_auto_id( + {"type": "backup/restore", "agent_id": "hassio.local", "backup_id": "abc123"} + ) + response = await client.receive_json() + assert response["event"] == { + "manager_state": "restore_backup", + "stage": None, + "state": "in_progress", + } + + supervisor_client.backups.partial_restore.assert_called_once_with( + "abc123", + supervisor_backups.PartialRestoreOptions( + addons=None, + background=True, + folders=None, + homeassistant=True, + location=None, + password=None, + ), + ) + + response = await client.receive_json() + assert response["event"] == { + "manager_state": "restore_backup", + "stage": None, + "state": "failed", + } + + response = await client.receive_json() + assert response["event"] == {"manager_state": "idle"} + + response = await client.receive_json() + assert response["error"]["code"] == expected_error_code + + @pytest.mark.parametrize( ("parameters", "expected_error"), [