mirror of
https://github.com/home-assistant/core.git
synced 2025-04-19 14:57:52 +00:00
Handle backup errors more consistently (#133522)
* Add backup manager and read writer errors * Clean up not needed default argument * Clean up todo comment * Trap agent bugs during upload * Always release stream * Clean up leftover * Update test for backup with automatic settings * Fix use of vol.Any * Refactor test helper * Only update successful timestamp if completed event is sent * Always delete surplus copies * Fix after rebase * Fix after rebase * Revert "Fix use of vol.Any" This reverts commit 28fd7a544899bb6ed05f771e9e608bc5b41d2b5e. * Inherit BackupReaderWriterError in IncorrectPasswordError --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
aa9e721e8b
commit
a329828bdf
@ -21,6 +21,7 @@ from .manager import (
|
||||
BackupManager,
|
||||
BackupPlatformProtocol,
|
||||
BackupReaderWriter,
|
||||
BackupReaderWriterError,
|
||||
CoreBackupReaderWriter,
|
||||
CreateBackupEvent,
|
||||
IncorrectPasswordError,
|
||||
@ -40,6 +41,7 @@ __all__ = [
|
||||
"BackupAgentPlatformProtocol",
|
||||
"BackupPlatformProtocol",
|
||||
"BackupReaderWriter",
|
||||
"BackupReaderWriterError",
|
||||
"CreateBackupEvent",
|
||||
"Folder",
|
||||
"IncorrectPasswordError",
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import LOGGER
|
||||
from .models import Folder
|
||||
from .models import BackupManagerError, Folder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import BackupManager, ManagerBackup
|
||||
@ -318,9 +318,9 @@ class BackupSchedule:
|
||||
password=config_data.create_backup.password,
|
||||
with_automatic_settings=True,
|
||||
)
|
||||
except BackupManagerError as err:
|
||||
LOGGER.error("Error creating backup: %s", err)
|
||||
except Exception: # noqa: BLE001
|
||||
# another more specific exception will be added
|
||||
# and handled in the future
|
||||
LOGGER.exception("Unexpected error creating automatic backup")
|
||||
|
||||
manager.remove_next_backup_event = async_track_point_in_time(
|
||||
|
@ -46,15 +46,11 @@ from .const import (
|
||||
EXCLUDE_FROM_BACKUP,
|
||||
LOGGER,
|
||||
)
|
||||
from .models import AgentBackup, Folder
|
||||
from .models import AgentBackup, BackupManagerError, Folder
|
||||
from .store import BackupStore
|
||||
from .util import make_backup_dir, read_backup, validate_password
|
||||
|
||||
|
||||
class IncorrectPasswordError(HomeAssistantError):
|
||||
"""Raised when the password is incorrect."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||
class NewBackup:
|
||||
"""New backup class."""
|
||||
@ -245,6 +241,14 @@ class BackupReaderWriter(abc.ABC):
|
||||
"""Restore a backup."""
|
||||
|
||||
|
||||
class BackupReaderWriterError(HomeAssistantError):
|
||||
"""Backup reader/writer error."""
|
||||
|
||||
|
||||
class IncorrectPasswordError(BackupReaderWriterError):
|
||||
"""Raised when the password is incorrect."""
|
||||
|
||||
|
||||
class BackupManager:
|
||||
"""Define the format that backup managers can have."""
|
||||
|
||||
@ -373,7 +377,9 @@ class BackupManager:
|
||||
)
|
||||
for result in pre_backup_results:
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
raise BackupManagerError(
|
||||
f"Error during pre-backup: {result}"
|
||||
) from result
|
||||
|
||||
async def async_post_backup_actions(self) -> None:
|
||||
"""Perform post backup actions."""
|
||||
@ -386,7 +392,9 @@ class BackupManager:
|
||||
)
|
||||
for result in post_backup_results:
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
raise BackupManagerError(
|
||||
f"Error during post-backup: {result}"
|
||||
) from result
|
||||
|
||||
async def load_platforms(self) -> None:
|
||||
"""Load backup platforms."""
|
||||
@ -422,11 +430,21 @@ class BackupManager:
|
||||
return_exceptions=True,
|
||||
)
|
||||
for idx, result in enumerate(sync_backup_results):
|
||||
if isinstance(result, Exception):
|
||||
if isinstance(result, BackupReaderWriterError):
|
||||
# writer errors will affect all agents
|
||||
# no point in continuing
|
||||
raise BackupManagerError(str(result)) from result
|
||||
if isinstance(result, BackupAgentError):
|
||||
agent_errors[agent_ids[idx]] = result
|
||||
LOGGER.exception(
|
||||
"Error during backup upload - %s", result, exc_info=result
|
||||
)
|
||||
continue
|
||||
if isinstance(result, Exception):
|
||||
# trap bugs from agents
|
||||
agent_errors[agent_ids[idx]] = result
|
||||
LOGGER.error("Unexpected error: %s", result, exc_info=result)
|
||||
continue
|
||||
if isinstance(result, BaseException):
|
||||
raise result
|
||||
|
||||
return agent_errors
|
||||
|
||||
async def async_get_backups(
|
||||
@ -449,7 +467,7 @@ class BackupManager:
|
||||
agent_errors[agent_ids[idx]] = result
|
||||
continue
|
||||
if isinstance(result, BaseException):
|
||||
raise result
|
||||
raise result # unexpected error
|
||||
for agent_backup in result:
|
||||
if (backup_id := agent_backup.backup_id) not in backups:
|
||||
if known_backup := self.known_backups.get(backup_id):
|
||||
@ -499,7 +517,7 @@ class BackupManager:
|
||||
agent_errors[agent_ids[idx]] = result
|
||||
continue
|
||||
if isinstance(result, BaseException):
|
||||
raise result
|
||||
raise result # unexpected error
|
||||
if not result:
|
||||
continue
|
||||
if backup is None:
|
||||
@ -563,7 +581,7 @@ class BackupManager:
|
||||
agent_errors[agent_ids[idx]] = result
|
||||
continue
|
||||
if isinstance(result, BaseException):
|
||||
raise result
|
||||
raise result # unexpected error
|
||||
|
||||
if not agent_errors:
|
||||
self.known_backups.remove(backup_id)
|
||||
@ -578,7 +596,7 @@ class BackupManager:
|
||||
) -> None:
|
||||
"""Receive and store a backup file from upload."""
|
||||
if self.state is not BackupManagerState.IDLE:
|
||||
raise HomeAssistantError(f"Backup manager busy: {self.state}")
|
||||
raise BackupManagerError(f"Backup manager busy: {self.state}")
|
||||
self.async_on_backup_event(
|
||||
ReceiveBackupEvent(stage=None, state=ReceiveBackupState.IN_PROGRESS)
|
||||
)
|
||||
@ -652,6 +670,7 @@ class BackupManager:
|
||||
include_homeassistant=include_homeassistant,
|
||||
name=name,
|
||||
password=password,
|
||||
raise_task_error=True,
|
||||
with_automatic_settings=with_automatic_settings,
|
||||
)
|
||||
assert self._backup_finish_task
|
||||
@ -669,11 +688,12 @@ class BackupManager:
|
||||
include_homeassistant: bool,
|
||||
name: str | None,
|
||||
password: str | None,
|
||||
raise_task_error: bool = False,
|
||||
with_automatic_settings: bool = False,
|
||||
) -> NewBackup:
|
||||
"""Initiate generating a backup."""
|
||||
if self.state is not BackupManagerState.IDLE:
|
||||
raise HomeAssistantError(f"Backup manager busy: {self.state}")
|
||||
raise BackupManagerError(f"Backup manager busy: {self.state}")
|
||||
|
||||
if with_automatic_settings:
|
||||
self.config.data.last_attempted_automatic_backup = dt_util.now()
|
||||
@ -692,6 +712,7 @@ class BackupManager:
|
||||
include_homeassistant=include_homeassistant,
|
||||
name=name,
|
||||
password=password,
|
||||
raise_task_error=raise_task_error,
|
||||
with_automatic_settings=with_automatic_settings,
|
||||
)
|
||||
except Exception:
|
||||
@ -714,15 +735,18 @@ class BackupManager:
|
||||
include_homeassistant: bool,
|
||||
name: str | None,
|
||||
password: str | None,
|
||||
raise_task_error: bool,
|
||||
with_automatic_settings: bool,
|
||||
) -> NewBackup:
|
||||
"""Initiate generating a backup."""
|
||||
if not agent_ids:
|
||||
raise HomeAssistantError("At least one agent must be selected")
|
||||
if any(agent_id not in self.backup_agents for agent_id in agent_ids):
|
||||
raise HomeAssistantError("Invalid agent selected")
|
||||
raise BackupManagerError("At least one agent must be selected")
|
||||
if invalid_agents := [
|
||||
agent_id for agent_id in agent_ids if agent_id not in self.backup_agents
|
||||
]:
|
||||
raise BackupManagerError(f"Invalid agents selected: {invalid_agents}")
|
||||
if include_all_addons and include_addons:
|
||||
raise HomeAssistantError(
|
||||
raise BackupManagerError(
|
||||
"Cannot include all addons and specify specific addons"
|
||||
)
|
||||
|
||||
@ -730,41 +754,64 @@ class BackupManager:
|
||||
name
|
||||
or f"{"Automatic" if with_automatic_settings else "Custom"} backup {HAVERSION}"
|
||||
)
|
||||
new_backup, self._backup_task = await self._reader_writer.async_create_backup(
|
||||
agent_ids=agent_ids,
|
||||
backup_name=backup_name,
|
||||
extra_metadata={
|
||||
"instance_id": await instance_id.async_get(self.hass),
|
||||
"with_automatic_settings": with_automatic_settings,
|
||||
},
|
||||
include_addons=include_addons,
|
||||
include_all_addons=include_all_addons,
|
||||
include_database=include_database,
|
||||
include_folders=include_folders,
|
||||
include_homeassistant=include_homeassistant,
|
||||
on_progress=self.async_on_backup_event,
|
||||
password=password,
|
||||
)
|
||||
self._backup_finish_task = self.hass.async_create_task(
|
||||
|
||||
try:
|
||||
(
|
||||
new_backup,
|
||||
self._backup_task,
|
||||
) = await self._reader_writer.async_create_backup(
|
||||
agent_ids=agent_ids,
|
||||
backup_name=backup_name,
|
||||
extra_metadata={
|
||||
"instance_id": await instance_id.async_get(self.hass),
|
||||
"with_automatic_settings": with_automatic_settings,
|
||||
},
|
||||
include_addons=include_addons,
|
||||
include_all_addons=include_all_addons,
|
||||
include_database=include_database,
|
||||
include_folders=include_folders,
|
||||
include_homeassistant=include_homeassistant,
|
||||
on_progress=self.async_on_backup_event,
|
||||
password=password,
|
||||
)
|
||||
except BackupReaderWriterError as err:
|
||||
raise BackupManagerError(str(err)) from err
|
||||
|
||||
backup_finish_task = self._backup_finish_task = self.hass.async_create_task(
|
||||
self._async_finish_backup(agent_ids, with_automatic_settings),
|
||||
name="backup_manager_finish_backup",
|
||||
)
|
||||
if not raise_task_error:
|
||||
|
||||
def log_finish_task_error(task: asyncio.Task[None]) -> None:
|
||||
if task.done() and not task.cancelled() and (err := task.exception()):
|
||||
if isinstance(err, BackupManagerError):
|
||||
LOGGER.error("Error creating backup: %s", err)
|
||||
else:
|
||||
LOGGER.error("Unexpected error: %s", err, exc_info=err)
|
||||
|
||||
backup_finish_task.add_done_callback(log_finish_task_error)
|
||||
|
||||
return new_backup
|
||||
|
||||
async def _async_finish_backup(
|
||||
self, agent_ids: list[str], with_automatic_settings: bool
|
||||
) -> None:
|
||||
"""Finish a backup."""
|
||||
if TYPE_CHECKING:
|
||||
assert self._backup_task is not None
|
||||
try:
|
||||
written_backup = await self._backup_task
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.debug("Generating backup failed", exc_info=err)
|
||||
except Exception as err:
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
|
||||
)
|
||||
if with_automatic_settings:
|
||||
self._update_issue_backup_failed()
|
||||
|
||||
if isinstance(err, BackupReaderWriterError):
|
||||
raise BackupManagerError(str(err)) from err
|
||||
raise # unexpected error
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Generated new backup with backup_id %s, uploading to agents %s",
|
||||
@ -777,25 +824,47 @@ class BackupManager:
|
||||
state=CreateBackupState.IN_PROGRESS,
|
||||
)
|
||||
)
|
||||
agent_errors = await self._async_upload_backup(
|
||||
backup=written_backup.backup,
|
||||
agent_ids=agent_ids,
|
||||
open_stream=written_backup.open_stream,
|
||||
)
|
||||
await written_backup.release_stream()
|
||||
if with_automatic_settings:
|
||||
# create backup was successful, update last_completed_automatic_backup
|
||||
self.config.data.last_completed_automatic_backup = dt_util.now()
|
||||
self.store.save()
|
||||
self._update_issue_after_agent_upload(agent_errors)
|
||||
self.known_backups.add(written_backup.backup, agent_errors)
|
||||
|
||||
try:
|
||||
agent_errors = await self._async_upload_backup(
|
||||
backup=written_backup.backup,
|
||||
agent_ids=agent_ids,
|
||||
open_stream=written_backup.open_stream,
|
||||
)
|
||||
except BaseException:
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
|
||||
)
|
||||
raise # manager or unexpected error
|
||||
finally:
|
||||
try:
|
||||
await written_backup.release_stream()
|
||||
except Exception:
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
|
||||
)
|
||||
raise
|
||||
self.known_backups.add(written_backup.backup, agent_errors)
|
||||
if agent_errors:
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.FAILED)
|
||||
)
|
||||
else:
|
||||
if with_automatic_settings:
|
||||
# create backup was successful, update last_completed_automatic_backup
|
||||
self.config.data.last_completed_automatic_backup = dt_util.now()
|
||||
self.store.save()
|
||||
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.COMPLETED)
|
||||
)
|
||||
|
||||
if with_automatic_settings:
|
||||
self._update_issue_after_agent_upload(agent_errors)
|
||||
# delete old backups more numerous than copies
|
||||
# try this regardless of agent errors above
|
||||
await delete_backups_exceeding_configured_count(self)
|
||||
|
||||
self.async_on_backup_event(
|
||||
CreateBackupEvent(stage=None, state=CreateBackupState.COMPLETED)
|
||||
)
|
||||
finally:
|
||||
self._backup_task = None
|
||||
self._backup_finish_task = None
|
||||
@ -814,7 +883,7 @@ class BackupManager:
|
||||
) -> None:
|
||||
"""Initiate restoring a backup."""
|
||||
if self.state is not BackupManagerState.IDLE:
|
||||
raise HomeAssistantError(f"Backup manager busy: {self.state}")
|
||||
raise BackupManagerError(f"Backup manager busy: {self.state}")
|
||||
|
||||
self.async_on_backup_event(
|
||||
RestoreBackupEvent(stage=None, state=RestoreBackupState.IN_PROGRESS)
|
||||
@ -854,7 +923,7 @@ class BackupManager:
|
||||
"""Initiate restoring a backup."""
|
||||
agent = self.backup_agents[agent_id]
|
||||
if not await agent.async_get_backup(backup_id):
|
||||
raise HomeAssistantError(
|
||||
raise BackupManagerError(
|
||||
f"Backup {backup_id} not found in agent {agent_id}"
|
||||
)
|
||||
|
||||
@ -1027,11 +1096,11 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
backup_id = _generate_backup_id(date_str, backup_name)
|
||||
|
||||
if include_addons or include_all_addons or include_folders:
|
||||
raise HomeAssistantError(
|
||||
raise BackupReaderWriterError(
|
||||
"Addons and folders are not supported by core backup"
|
||||
)
|
||||
if not include_homeassistant:
|
||||
raise HomeAssistantError("Home Assistant must be included in backup")
|
||||
raise BackupReaderWriterError("Home Assistant must be included in backup")
|
||||
|
||||
backup_task = self._hass.async_create_task(
|
||||
self._async_create_backup(
|
||||
@ -1102,6 +1171,13 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
password,
|
||||
local_agent_tar_file_path,
|
||||
)
|
||||
except (BackupManagerError, OSError, tarfile.TarError, ValueError) as err:
|
||||
# BackupManagerError from async_pre_backup_actions
|
||||
# OSError from file operations
|
||||
# TarError from tarfile
|
||||
# ValueError from json_bytes
|
||||
raise BackupReaderWriterError(str(err)) from err
|
||||
else:
|
||||
backup = AgentBackup(
|
||||
addons=[],
|
||||
backup_id=backup_id,
|
||||
@ -1119,12 +1195,15 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
async_add_executor_job = self._hass.async_add_executor_job
|
||||
|
||||
async def send_backup() -> AsyncIterator[bytes]:
|
||||
f = await async_add_executor_job(tar_file_path.open, "rb")
|
||||
try:
|
||||
while chunk := await async_add_executor_job(f.read, 2**20):
|
||||
yield chunk
|
||||
finally:
|
||||
await async_add_executor_job(f.close)
|
||||
f = await async_add_executor_job(tar_file_path.open, "rb")
|
||||
try:
|
||||
while chunk := await async_add_executor_job(f.read, 2**20):
|
||||
yield chunk
|
||||
finally:
|
||||
await async_add_executor_job(f.close)
|
||||
except OSError as err:
|
||||
raise BackupReaderWriterError(str(err)) from err
|
||||
|
||||
async def open_backup() -> AsyncIterator[bytes]:
|
||||
return send_backup()
|
||||
@ -1132,14 +1211,20 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
async def remove_backup() -> None:
|
||||
if local_agent_tar_file_path:
|
||||
return
|
||||
await async_add_executor_job(tar_file_path.unlink, True)
|
||||
try:
|
||||
await async_add_executor_job(tar_file_path.unlink, True)
|
||||
except OSError as err:
|
||||
raise BackupReaderWriterError(str(err)) from err
|
||||
|
||||
return WrittenBackup(
|
||||
backup=backup, open_stream=open_backup, release_stream=remove_backup
|
||||
)
|
||||
finally:
|
||||
# Inform integrations the backup is done
|
||||
await manager.async_post_backup_actions()
|
||||
try:
|
||||
await manager.async_post_backup_actions()
|
||||
except BackupManagerError as err:
|
||||
raise BackupReaderWriterError(str(err)) from err
|
||||
|
||||
def _mkdir_and_generate_backup_contents(
|
||||
self,
|
||||
@ -1252,11 +1337,11 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
"""
|
||||
|
||||
if restore_addons or restore_folders:
|
||||
raise HomeAssistantError(
|
||||
raise BackupReaderWriterError(
|
||||
"Addons and folders are not supported in core restore"
|
||||
)
|
||||
if not restore_homeassistant and not restore_database:
|
||||
raise HomeAssistantError(
|
||||
raise BackupReaderWriterError(
|
||||
"Home Assistant or database must be included in restore"
|
||||
)
|
||||
|
||||
|
@ -6,6 +6,8 @@ from dataclasses import asdict, dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any, Self
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AddonInfo:
|
||||
@ -67,3 +69,7 @@ class AgentBackup:
|
||||
protected=data["protected"],
|
||||
size=data["size"],
|
||||
)
|
||||
|
||||
|
||||
class BackupManagerError(HomeAssistantError):
|
||||
"""Backup manager error."""
|
||||
|
@ -10,6 +10,7 @@ from typing import Any, cast
|
||||
|
||||
from aiohasupervisor.exceptions import (
|
||||
SupervisorBadRequestError,
|
||||
SupervisorError,
|
||||
SupervisorNotFoundError,
|
||||
)
|
||||
from aiohasupervisor.models import (
|
||||
@ -23,6 +24,7 @@ from homeassistant.components.backup import (
|
||||
AgentBackup,
|
||||
BackupAgent,
|
||||
BackupReaderWriter,
|
||||
BackupReaderWriterError,
|
||||
CreateBackupEvent,
|
||||
Folder,
|
||||
IncorrectPasswordError,
|
||||
@ -234,20 +236,23 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
||||
]
|
||||
locations = [agent.location for agent in hassio_agents]
|
||||
|
||||
backup = await self._client.backups.partial_backup(
|
||||
supervisor_backups.PartialBackupOptions(
|
||||
addons=include_addons_set,
|
||||
folders=include_folders_set,
|
||||
homeassistant=include_homeassistant,
|
||||
name=backup_name,
|
||||
password=password,
|
||||
compressed=True,
|
||||
location=locations or LOCATION_CLOUD_BACKUP,
|
||||
homeassistant_exclude_database=not include_database,
|
||||
background=True,
|
||||
extra=extra_metadata,
|
||||
try:
|
||||
backup = await self._client.backups.partial_backup(
|
||||
supervisor_backups.PartialBackupOptions(
|
||||
addons=include_addons_set,
|
||||
folders=include_folders_set,
|
||||
homeassistant=include_homeassistant,
|
||||
name=backup_name,
|
||||
password=password,
|
||||
compressed=True,
|
||||
location=locations or LOCATION_CLOUD_BACKUP,
|
||||
homeassistant_exclude_database=not include_database,
|
||||
background=True,
|
||||
extra=extra_metadata,
|
||||
)
|
||||
)
|
||||
)
|
||||
except SupervisorError as err:
|
||||
raise BackupReaderWriterError(f"Error creating backup: {err}") from err
|
||||
backup_task = self._hass.async_create_task(
|
||||
self._async_wait_for_backup(
|
||||
backup, remove_after_upload=not bool(locations)
|
||||
@ -279,22 +284,35 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
||||
finally:
|
||||
unsub()
|
||||
if not backup_id:
|
||||
raise HomeAssistantError("Backup failed")
|
||||
raise BackupReaderWriterError("Backup failed")
|
||||
|
||||
async def open_backup() -> AsyncIterator[bytes]:
|
||||
return await self._client.backups.download_backup(backup_id)
|
||||
try:
|
||||
return await self._client.backups.download_backup(backup_id)
|
||||
except SupervisorError as err:
|
||||
raise BackupReaderWriterError(
|
||||
f"Error downloading backup: {err}"
|
||||
) from err
|
||||
|
||||
async def remove_backup() -> None:
|
||||
if not remove_after_upload:
|
||||
return
|
||||
await self._client.backups.remove_backup(
|
||||
backup_id,
|
||||
options=supervisor_backups.RemoveBackupOptions(
|
||||
location={LOCATION_CLOUD_BACKUP}
|
||||
),
|
||||
)
|
||||
try:
|
||||
await self._client.backups.remove_backup(
|
||||
backup_id,
|
||||
options=supervisor_backups.RemoveBackupOptions(
|
||||
location={LOCATION_CLOUD_BACKUP}
|
||||
),
|
||||
)
|
||||
except SupervisorError as err:
|
||||
raise BackupReaderWriterError(f"Error removing backup: {err}") from err
|
||||
|
||||
details = await self._client.backups.backup_info(backup_id)
|
||||
try:
|
||||
details = await self._client.backups.backup_info(backup_id)
|
||||
except SupervisorError as err:
|
||||
raise BackupReaderWriterError(
|
||||
f"Error getting backup details: {err}"
|
||||
) from err
|
||||
|
||||
return WrittenBackup(
|
||||
backup=_backup_details_to_agent_backup(details),
|
||||
|
@ -166,3 +166,15 @@ async def setup_backup_integration(
|
||||
agent._loaded_backups = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def setup_backup_platform(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
domain: str,
|
||||
platform: Any,
|
||||
) -> None:
|
||||
"""Set up a mock domain."""
|
||||
mock_platform(hass, f"{domain}.backup", platform)
|
||||
assert await async_setup_component(hass, domain, {})
|
||||
await hass.async_block_till_done()
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
from dataclasses import replace
|
||||
from io import StringIO
|
||||
import json
|
||||
from pathlib import Path
|
||||
@ -17,13 +18,15 @@ from homeassistant.components.backup import (
|
||||
AgentBackup,
|
||||
BackupAgentPlatformProtocol,
|
||||
BackupManager,
|
||||
BackupPlatformProtocol,
|
||||
BackupReaderWriterError,
|
||||
Folder,
|
||||
LocalBackupAgent,
|
||||
backup as local_backup_platform,
|
||||
)
|
||||
from homeassistant.components.backup.agent import BackupAgentError
|
||||
from homeassistant.components.backup.const import DATA_MANAGER
|
||||
from homeassistant.components.backup.manager import (
|
||||
BackupManagerError,
|
||||
BackupManagerState,
|
||||
CoreBackupReaderWriter,
|
||||
CreateBackupEvent,
|
||||
@ -42,9 +45,9 @@ from .common import (
|
||||
TEST_BACKUP_ABC123,
|
||||
TEST_BACKUP_DEF456,
|
||||
BackupAgentTest,
|
||||
setup_backup_platform,
|
||||
)
|
||||
|
||||
from tests.common import MockPlatform, mock_platform
|
||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||
|
||||
_EXPECTED_FILES = [
|
||||
@ -61,18 +64,6 @@ _EXPECTED_FILES_WITH_DATABASE = {
|
||||
}
|
||||
|
||||
|
||||
async def _setup_backup_platform(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
domain: str = "some_domain",
|
||||
platform: BackupPlatformProtocol | BackupAgentPlatformProtocol | None = None,
|
||||
) -> None:
|
||||
"""Set up a mock domain."""
|
||||
mock_platform(hass, f"{domain}.backup", platform or MockPlatform())
|
||||
assert await async_setup_component(hass, domain, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_delay_save() -> Generator[None]:
|
||||
"""Mock the delay save constant."""
|
||||
@ -159,12 +150,15 @@ async def test_async_create_backup_when_backing_up(hass: HomeAssistant) -> None:
|
||||
("parameters", "expected_error"),
|
||||
[
|
||||
({"agent_ids": []}, "At least one agent must be selected"),
|
||||
({"agent_ids": ["non_existing"]}, "Invalid agent selected"),
|
||||
({"agent_ids": ["non_existing"]}, "Invalid agents selected: ['non_existing']"),
|
||||
(
|
||||
{"include_addons": ["ssl"], "include_all_addons": True},
|
||||
"Cannot include all addons and specify specific addons",
|
||||
),
|
||||
({"include_homeassistant": False}, "Home Assistant must be included in backup"),
|
||||
(
|
||||
{"include_homeassistant": False},
|
||||
"Home Assistant must be included in backup",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_create_backup_wrong_parameters(
|
||||
@ -242,7 +236,7 @@ async def test_async_initiate_backup(
|
||||
core_get_backup_agents.return_value = [local_agent]
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -393,19 +387,96 @@ async def test_async_initiate_backup(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
@pytest.mark.parametrize("exception", [BackupAgentError("Boom!"), Exception("Boom!")])
|
||||
async def test_async_initiate_backup_with_agent_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
mocked_json_bytes: Mock,
|
||||
mocked_tarfile: Mock,
|
||||
generate_backup_id: MagicMock,
|
||||
path_glob: MagicMock,
|
||||
hass_storage: dict[str, Any],
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test generate backup."""
|
||||
"""Test agent upload error during backup generation."""
|
||||
agent_ids = [LOCAL_AGENT_ID, "test.remote"]
|
||||
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
backup_1 = replace(TEST_BACKUP_ABC123, backup_id="backup1") # matching instance id
|
||||
backup_2 = replace(TEST_BACKUP_DEF456, backup_id="backup2") # other instance id
|
||||
backup_3 = replace(TEST_BACKUP_ABC123, backup_id="backup3") # matching instance id
|
||||
backups_info: list[dict[str, Any]] = [
|
||||
{
|
||||
"addons": [
|
||||
{
|
||||
"name": "Test",
|
||||
"slug": "test",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
],
|
||||
"agent_ids": [
|
||||
"test.remote",
|
||||
],
|
||||
"backup_id": "backup1",
|
||||
"database_included": True,
|
||||
"date": "1970-01-01T00:00:00.000Z",
|
||||
"failed_agent_ids": [],
|
||||
"folders": [
|
||||
"media",
|
||||
"share",
|
||||
],
|
||||
"homeassistant_included": True,
|
||||
"homeassistant_version": "2024.12.0",
|
||||
"name": "Test",
|
||||
"protected": False,
|
||||
"size": 0,
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
{
|
||||
"addons": [],
|
||||
"agent_ids": [
|
||||
"test.remote",
|
||||
],
|
||||
"backup_id": "backup2",
|
||||
"database_included": False,
|
||||
"date": "1980-01-01T00:00:00.000Z",
|
||||
"failed_agent_ids": [],
|
||||
"folders": [
|
||||
"media",
|
||||
"share",
|
||||
],
|
||||
"homeassistant_included": True,
|
||||
"homeassistant_version": "2024.12.0",
|
||||
"name": "Test 2",
|
||||
"protected": False,
|
||||
"size": 1,
|
||||
"with_automatic_settings": None,
|
||||
},
|
||||
{
|
||||
"addons": [
|
||||
{
|
||||
"name": "Test",
|
||||
"slug": "test",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
],
|
||||
"agent_ids": [
|
||||
"test.remote",
|
||||
],
|
||||
"backup_id": "backup3",
|
||||
"database_included": True,
|
||||
"date": "1970-01-01T00:00:00.000Z",
|
||||
"failed_agent_ids": [],
|
||||
"folders": [
|
||||
"media",
|
||||
"share",
|
||||
],
|
||||
"homeassistant_included": True,
|
||||
"homeassistant_version": "2024.12.0",
|
||||
"name": "Test",
|
||||
"protected": False,
|
||||
"size": 0,
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
]
|
||||
remote_agent = BackupAgentTest("remote", backups=[backup_1, backup_2, backup_3])
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.backup.async_get_backup_agents"
|
||||
@ -413,7 +484,7 @@ async def test_async_initiate_backup_with_agent_error(
|
||||
core_get_backup_agents.return_value = [local_agent]
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -431,12 +502,18 @@ async def test_async_initiate_backup_with_agent_error(
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["result"] == {
|
||||
"backups": [],
|
||||
"backups": backups_info,
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "backup/config/update", "retention": {"copies": 1, "days": None}}
|
||||
)
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"]
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
@ -445,11 +522,16 @@ async def test_async_initiate_backup_with_agent_error(
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
|
||||
delete_backup = AsyncMock()
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.open", mock_open(read_data=b"test")),
|
||||
patch.object(
|
||||
remote_agent, "async_upload_backup", side_effect=Exception("Test exception")
|
||||
remote_agent,
|
||||
"async_upload_backup",
|
||||
side_effect=exception,
|
||||
),
|
||||
patch.object(remote_agent, "async_delete_backup", delete_backup),
|
||||
):
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "backup/generate", "agent_ids": agent_ids}
|
||||
@ -486,13 +568,13 @@ async def test_async_initiate_backup_with_agent_error(
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": None,
|
||||
"state": CreateBackupState.COMPLETED,
|
||||
"state": CreateBackupState.FAILED,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
|
||||
|
||||
expected_backup_data = {
|
||||
new_expected_backup_data = {
|
||||
"addons": [],
|
||||
"agent_ids": ["backup.local"],
|
||||
"backup_id": "abc123",
|
||||
@ -508,20 +590,14 @@ async def test_async_initiate_backup_with_agent_error(
|
||||
"with_automatic_settings": False,
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "backup/details", "backup_id": backup_id}
|
||||
)
|
||||
result = await ws_client.receive_json()
|
||||
assert result["result"] == {
|
||||
"agent_errors": {},
|
||||
"backup": expected_backup_data,
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/info"})
|
||||
result = await ws_client.receive_json()
|
||||
backups_response = result["result"].pop("backups")
|
||||
|
||||
assert len(backups_response) == 4
|
||||
assert new_expected_backup_data in backups_response
|
||||
assert result["result"] == {
|
||||
"agent_errors": {},
|
||||
"backups": [expected_backup_data],
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
}
|
||||
@ -534,6 +610,9 @@ async def test_async_initiate_backup_with_agent_error(
|
||||
}
|
||||
]
|
||||
|
||||
# one of the two matching backups with the remote agent should have been deleted
|
||||
assert delete_backup.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
@pytest.mark.parametrize(
|
||||
@ -702,7 +781,7 @@ async def test_create_backup_failure_raises_issue(
|
||||
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -743,6 +822,337 @@ async def test_create_backup_failure_raises_issue(
|
||||
assert issue.translation_placeholders == issue_data["translation_placeholders"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
@pytest.mark.parametrize(
|
||||
"exception", [BackupReaderWriterError("Boom!"), BaseException("Boom!")]
|
||||
)
|
||||
async def test_async_initiate_backup_non_agent_upload_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
generate_backup_id: MagicMock,
|
||||
path_glob: MagicMock,
|
||||
hass_storage: dict[str, Any],
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test an unknown or writer upload error during backup generation."""
|
||||
hass_storage[DOMAIN] = {
|
||||
"data": {},
|
||||
"key": DOMAIN,
|
||||
"version": 1,
|
||||
}
|
||||
agent_ids = [LOCAL_AGENT_ID, "test.remote"]
|
||||
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.backup.async_get_backup_agents"
|
||||
) as core_get_backup_agents:
|
||||
core_get_backup_agents.return_value = [local_agent]
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
|
||||
spec_set=BackupAgentPlatformProtocol,
|
||||
),
|
||||
)
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
path_glob.return_value = []
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/info"})
|
||||
result = await ws_client.receive_json()
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["result"] == {
|
||||
"backups": [],
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.open", mock_open(read_data=b"test")),
|
||||
patch.object(
|
||||
remote_agent,
|
||||
"async_upload_backup",
|
||||
side_effect=exception,
|
||||
),
|
||||
):
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "backup/generate", "agent_ids": agent_ids}
|
||||
)
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": None,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
|
||||
backup_id = result["result"]["backup_job_id"]
|
||||
assert backup_id == generate_backup_id.return_value
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": CreateBackupStage.HOME_ASSISTANT,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": CreateBackupStage.UPLOAD_TO_AGENTS,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": None,
|
||||
"state": CreateBackupState.FAILED,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
|
||||
|
||||
assert not hass_storage[DOMAIN]["data"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
@pytest.mark.parametrize(
|
||||
"exception", [BackupReaderWriterError("Boom!"), Exception("Boom!")]
|
||||
)
|
||||
async def test_async_initiate_backup_with_task_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
generate_backup_id: MagicMock,
|
||||
path_glob: MagicMock,
|
||||
create_backup: AsyncMock,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test backup task error during backup generation."""
|
||||
backup_task: asyncio.Future[Any] = asyncio.Future()
|
||||
backup_task.set_exception(exception)
|
||||
create_backup.return_value = (NewBackup(backup_job_id="abc123"), backup_task)
|
||||
agent_ids = [LOCAL_AGENT_ID, "test.remote"]
|
||||
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.backup.async_get_backup_agents"
|
||||
) as core_get_backup_agents:
|
||||
core_get_backup_agents.return_value = [local_agent]
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
|
||||
spec_set=BackupAgentPlatformProtocol,
|
||||
),
|
||||
)
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
path_glob.return_value = []
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/info"})
|
||||
result = await ws_client.receive_json()
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["result"] == {
|
||||
"backups": [],
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "backup/generate", "agent_ids": agent_ids}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": None,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": None,
|
||||
"state": CreateBackupState.FAILED,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
|
||||
backup_id = result["result"]["backup_job_id"]
|
||||
assert backup_id == generate_backup_id.return_value
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"open_call_count",
|
||||
"open_exception",
|
||||
"read_call_count",
|
||||
"read_exception",
|
||||
"close_call_count",
|
||||
"close_exception",
|
||||
"unlink_call_count",
|
||||
"unlink_exception",
|
||||
),
|
||||
[
|
||||
(1, OSError("Boom!"), 0, None, 0, None, 1, None),
|
||||
(1, None, 1, OSError("Boom!"), 1, None, 1, None),
|
||||
(1, None, 1, None, 1, OSError("Boom!"), 1, None),
|
||||
(1, None, 1, None, 1, None, 1, OSError("Boom!")),
|
||||
],
|
||||
)
|
||||
async def test_initiate_backup_file_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
generate_backup_id: MagicMock,
|
||||
path_glob: MagicMock,
|
||||
open_call_count: int,
|
||||
open_exception: Exception | None,
|
||||
read_call_count: int,
|
||||
read_exception: Exception | None,
|
||||
close_call_count: int,
|
||||
close_exception: Exception | None,
|
||||
unlink_call_count: int,
|
||||
unlink_exception: Exception | None,
|
||||
) -> None:
|
||||
"""Test file error during generate backup."""
|
||||
agent_ids = ["test.remote"]
|
||||
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
with patch(
|
||||
"homeassistant.components.backup.backup.async_get_backup_agents"
|
||||
) as core_get_backup_agents:
|
||||
core_get_backup_agents.return_value = [local_agent]
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
|
||||
spec_set=BackupAgentPlatformProtocol,
|
||||
),
|
||||
)
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
path_glob.return_value = []
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/info"})
|
||||
result = await ws_client.receive_json()
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["result"] == {
|
||||
"backups": [],
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
|
||||
open_mock = mock_open(read_data=b"test")
|
||||
open_mock.side_effect = open_exception
|
||||
open_mock.return_value.read.side_effect = read_exception
|
||||
open_mock.return_value.close.side_effect = close_exception
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.open", open_mock),
|
||||
patch("pathlib.Path.unlink", side_effect=unlink_exception) as unlink_mock,
|
||||
):
|
||||
await ws_client.send_json_auto_id(
|
||||
{"type": "backup/generate", "agent_ids": agent_ids}
|
||||
)
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": None,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
|
||||
backup_id = result["result"]["backup_job_id"]
|
||||
assert backup_id == generate_backup_id.return_value
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": CreateBackupStage.HOME_ASSISTANT,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": CreateBackupStage.UPLOAD_TO_AGENTS,
|
||||
"state": CreateBackupState.IN_PROGRESS,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {
|
||||
"manager_state": BackupManagerState.CREATE_BACKUP,
|
||||
"stage": None,
|
||||
"state": CreateBackupState.FAILED,
|
||||
}
|
||||
|
||||
result = await ws_client.receive_json()
|
||||
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
|
||||
|
||||
assert open_mock.call_count == open_call_count
|
||||
assert open_mock.return_value.read.call_count == read_call_count
|
||||
assert open_mock.return_value.close.call_count == close_call_count
|
||||
assert unlink_mock.call_count == unlink_call_count
|
||||
|
||||
|
||||
async def test_loading_platforms(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@ -754,8 +1164,9 @@ async def test_loading_platforms(
|
||||
|
||||
get_agents_mock = AsyncMock(return_value=[])
|
||||
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
@ -776,7 +1187,7 @@ class LocalBackupAgentTest(BackupAgentTest, LocalBackupAgent):
|
||||
|
||||
def get_backup_path(self, backup_id: str) -> Path:
|
||||
"""Return the local path to a backup."""
|
||||
return "test.tar"
|
||||
return Path("test.tar")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -797,7 +1208,7 @@ async def test_loading_platform_with_listener(
|
||||
get_agents_mock = AsyncMock(return_value=[agent_class("remote1", backups=[])])
|
||||
register_listener_mock = Mock()
|
||||
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -846,7 +1257,7 @@ async def test_not_loading_bad_platforms(
|
||||
platform_mock: Mock,
|
||||
) -> None:
|
||||
"""Test not loading bad backup platforms."""
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=platform_mock,
|
||||
@ -857,16 +1268,14 @@ async def test_not_loading_bad_platforms(
|
||||
assert platform_mock.mock_calls == []
|
||||
|
||||
|
||||
async def test_exception_platform_pre(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
async def test_exception_platform_pre(hass: HomeAssistant) -> None:
|
||||
"""Test exception in pre step."""
|
||||
|
||||
async def _mock_step(hass: HomeAssistant) -> None:
|
||||
raise HomeAssistantError("Test exception")
|
||||
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -878,28 +1287,25 @@ async def test_exception_platform_pre(
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"create",
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(BackupManagerError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"create",
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert "Generating backup failed" in caplog.text
|
||||
assert "Test exception" in caplog.text
|
||||
assert str(err.value) == "Error during pre-backup: Test exception"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
async def test_exception_platform_post(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
async def test_exception_platform_post(hass: HomeAssistant) -> None:
|
||||
"""Test exception in post step."""
|
||||
|
||||
async def _mock_step(hass: HomeAssistant) -> None:
|
||||
raise HomeAssistantError("Test exception")
|
||||
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -911,14 +1317,14 @@ async def test_exception_platform_post(
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"create",
|
||||
blocking=True,
|
||||
)
|
||||
with pytest.raises(BackupManagerError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"create",
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert "Generating backup failed" in caplog.text
|
||||
assert "Test exception" in caplog.text
|
||||
assert str(err.value) == "Error during post-backup: Test exception"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -974,7 +1380,7 @@ async def test_receive_backup(
|
||||
) -> None:
|
||||
"""Test receive backup and upload to the local and a remote agent."""
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -1098,8 +1504,8 @@ async def test_async_trigger_restore(
|
||||
manager = BackupManager(hass, CoreBackupReaderWriter(hass))
|
||||
hass.data[DATA_MANAGER] = manager
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -1156,8 +1562,8 @@ async def test_async_trigger_restore_wrong_password(hass: HomeAssistant) -> None
|
||||
manager = BackupManager(hass, CoreBackupReaderWriter(hass))
|
||||
hass.data[DATA_MANAGER] = manager
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await _setup_backup_platform(
|
||||
await setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
@ -1228,7 +1634,7 @@ async def test_async_trigger_restore_wrong_parameters(
|
||||
"""Test trigger restore."""
|
||||
manager = BackupManager(hass, CoreBackupReaderWriter(hass))
|
||||
|
||||
await _setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await setup_backup_platform(hass, domain=DOMAIN, platform=local_backup_platform)
|
||||
await manager.load_platforms()
|
||||
|
||||
local_agent = manager.backup_agents[LOCAL_AGENT_ID]
|
||||
|
@ -2,13 +2,19 @@
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, call, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.backup import AgentBackup, BackupAgentError
|
||||
from homeassistant.components.backup import (
|
||||
AgentBackup,
|
||||
BackupAgentError,
|
||||
BackupAgentPlatformProtocol,
|
||||
BackupReaderWriterError,
|
||||
Folder,
|
||||
)
|
||||
from homeassistant.components.backup.agent import BackupAgentUnreachableError
|
||||
from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN
|
||||
from homeassistant.components.backup.manager import (
|
||||
@ -19,6 +25,7 @@ from homeassistant.components.backup.manager import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import (
|
||||
LOCAL_AGENT_ID,
|
||||
@ -26,6 +33,7 @@ from .common import (
|
||||
TEST_BACKUP_DEF456,
|
||||
BackupAgentTest,
|
||||
setup_backup_integration,
|
||||
setup_backup_platform,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed, async_mock_service
|
||||
@ -472,27 +480,45 @@ async def test_generate_calls_create(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
@pytest.mark.parametrize(
|
||||
("create_backup_settings", "expected_call_params"),
|
||||
(
|
||||
"create_backup_settings",
|
||||
"expected_call_params",
|
||||
"side_effect",
|
||||
"last_completed_automatic_backup",
|
||||
),
|
||||
[
|
||||
(
|
||||
{},
|
||||
{
|
||||
"agent_ids": [],
|
||||
"agent_ids": ["test.remote"],
|
||||
"include_addons": None,
|
||||
"include_all_addons": False,
|
||||
"include_database": True,
|
||||
"include_folders": None,
|
||||
"name": None,
|
||||
"password": None,
|
||||
},
|
||||
{
|
||||
"agent_ids": ["test.remote"],
|
||||
"backup_name": ANY,
|
||||
"extra_metadata": {
|
||||
"instance_id": ANY,
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
"include_addons": None,
|
||||
"include_all_addons": False,
|
||||
"include_database": True,
|
||||
"include_folders": None,
|
||||
"include_homeassistant": True,
|
||||
"name": None,
|
||||
"on_progress": ANY,
|
||||
"password": None,
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
None,
|
||||
"2024-11-13T12:01:01+01:00",
|
||||
),
|
||||
(
|
||||
{
|
||||
"agent_ids": ["test-agent"],
|
||||
"agent_ids": ["test.remote"],
|
||||
"include_addons": ["test-addon"],
|
||||
"include_all_addons": False,
|
||||
"include_database": True,
|
||||
@ -501,32 +527,78 @@ async def test_generate_calls_create(
|
||||
"password": "test-password",
|
||||
},
|
||||
{
|
||||
"agent_ids": ["test-agent"],
|
||||
"agent_ids": ["test.remote"],
|
||||
"backup_name": "test-name",
|
||||
"extra_metadata": {
|
||||
"instance_id": ANY,
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
"include_addons": ["test-addon"],
|
||||
"include_all_addons": False,
|
||||
"include_database": True,
|
||||
"include_folders": ["media"],
|
||||
"include_folders": [Folder.MEDIA],
|
||||
"include_homeassistant": True,
|
||||
"name": "test-name",
|
||||
"on_progress": ANY,
|
||||
"password": "test-password",
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
None,
|
||||
"2024-11-13T12:01:01+01:00",
|
||||
),
|
||||
(
|
||||
{
|
||||
"agent_ids": ["test.remote"],
|
||||
"include_addons": None,
|
||||
"include_all_addons": False,
|
||||
"include_database": True,
|
||||
"include_folders": None,
|
||||
"name": None,
|
||||
"password": None,
|
||||
},
|
||||
{
|
||||
"agent_ids": ["test.remote"],
|
||||
"backup_name": ANY,
|
||||
"extra_metadata": {
|
||||
"instance_id": ANY,
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
"include_addons": None,
|
||||
"include_all_addons": False,
|
||||
"include_database": True,
|
||||
"include_folders": None,
|
||||
"include_homeassistant": True,
|
||||
"on_progress": ANY,
|
||||
"password": None,
|
||||
},
|
||||
BackupAgentError("Boom!"),
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_generate_with_default_settings_calls_create(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
hass_storage: dict[str, Any],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
create_backup: AsyncMock,
|
||||
create_backup_settings: dict[str, Any],
|
||||
expected_call_params: dict[str, Any],
|
||||
side_effect: Exception | None,
|
||||
last_completed_automatic_backup: str,
|
||||
) -> None:
|
||||
"""Test backup/generate_with_automatic_settings calls async_initiate_backup."""
|
||||
await setup_backup_integration(hass, with_hassio=False)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
freezer.move_to("2024-11-13 12:01:00+01:00")
|
||||
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
||||
freezer.move_to("2024-11-13T12:01:00+01:00")
|
||||
remote_agent = BackupAgentTest("remote", backups=[])
|
||||
await setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
|
||||
spec_set=BackupAgentPlatformProtocol,
|
||||
),
|
||||
)
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json_auto_id(
|
||||
@ -535,17 +607,47 @@ async def test_generate_with_default_settings_calls_create(
|
||||
result = await client.receive_json()
|
||||
assert result["success"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_initiate_backup",
|
||||
return_value=NewBackup(backup_job_id="abc123"),
|
||||
) as generate_backup:
|
||||
freezer.tick()
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass_storage[DOMAIN]["data"]["config"]["create_backup"]
|
||||
== create_backup_settings
|
||||
)
|
||||
assert (
|
||||
hass_storage[DOMAIN]["data"]["config"]["last_attempted_automatic_backup"]
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
hass_storage[DOMAIN]["data"]["config"]["last_completed_automatic_backup"]
|
||||
is None
|
||||
)
|
||||
|
||||
with patch.object(remote_agent, "async_upload_backup", side_effect=side_effect):
|
||||
await client.send_json_auto_id(
|
||||
{"type": "backup/generate_with_automatic_settings"}
|
||||
)
|
||||
result = await client.receive_json()
|
||||
assert result["success"]
|
||||
assert result["result"] == {"backup_job_id": "abc123"}
|
||||
generate_backup.assert_called_once_with(**expected_call_params)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
create_backup.assert_called_once_with(**expected_call_params)
|
||||
|
||||
freezer.tick()
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass_storage[DOMAIN]["data"]["config"]["last_attempted_automatic_backup"]
|
||||
== "2024-11-13T12:01:01+01:00"
|
||||
)
|
||||
assert (
|
||||
hass_storage[DOMAIN]["data"]["config"]["last_completed_automatic_backup"]
|
||||
== last_completed_automatic_backup
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -1193,7 +1295,23 @@ async def test_config_update_errors(
|
||||
1,
|
||||
2,
|
||||
BACKUP_CALL,
|
||||
[Exception("Boom"), None],
|
||||
[BackupReaderWriterError("Boom"), None],
|
||||
),
|
||||
(
|
||||
{
|
||||
"type": "backup/config/update",
|
||||
"create_backup": {"agent_ids": ["test.test-agent"]},
|
||||
"schedule": "daily",
|
||||
},
|
||||
"2024-11-11T04:45:00+01:00",
|
||||
"2024-11-12T04:45:00+01:00",
|
||||
"2024-11-13T04:45:00+01:00",
|
||||
"2024-11-12T04:45:00+01:00", # attempted to create backup but failed
|
||||
"2024-11-11T04:45:00+01:00",
|
||||
1,
|
||||
2,
|
||||
BACKUP_CALL,
|
||||
[Exception("Boom"), None], # unknown error
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -2272,7 +2390,7 @@ async def test_subscribe_event(
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test generating a backup."""
|
||||
"""Test subscribe event."""
|
||||
await setup_backup_integration(hass, with_hassio=False)
|
||||
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
|
@ -35,7 +35,10 @@ async def setup_integration(
|
||||
cloud_logged_in: None,
|
||||
) -> AsyncGenerator[None]:
|
||||
"""Set up cloud integration."""
|
||||
with patch("homeassistant.components.backup.is_hassio", return_value=False):
|
||||
with (
|
||||
patch("homeassistant.components.backup.is_hassio", return_value=False),
|
||||
patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0),
|
||||
):
|
||||
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
@ -345,7 +348,7 @@ async def test_agents_upload(
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
protected=True,
|
||||
size=0.0,
|
||||
size=0,
|
||||
)
|
||||
aioclient_mock.put(mock_get_upload_details.return_value["url"])
|
||||
|
||||
@ -382,7 +385,7 @@ async def test_agents_upload(
|
||||
async def test_agents_upload_fail_put(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
hass_storage: dict[str, Any],
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
mock_get_upload_details: Mock,
|
||||
put_mock_kwargs: dict[str, Any],
|
||||
@ -401,7 +404,7 @@ async def test_agents_upload_fail_put(
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
protected=True,
|
||||
size=0.0,
|
||||
size=0,
|
||||
)
|
||||
aioclient_mock.put(mock_get_upload_details.return_value["url"], **put_mock_kwargs)
|
||||
|
||||
@ -421,9 +424,14 @@ async def test_agents_upload_fail_put(
|
||||
"/api/backup/upload?agent_id=cloud.cloud",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert resp.status == 201
|
||||
assert "Error during backup upload - Failed to upload backup" in caplog.text
|
||||
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
|
||||
assert len(store_backups) == 1
|
||||
stored_backup = store_backups[0]
|
||||
assert stored_backup["backup_id"] == backup_id
|
||||
assert stored_backup["failed_agent_ids"] == ["cloud.cloud"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("side_effect", [ClientError, CloudError])
|
||||
@ -431,9 +439,9 @@ async def test_agents_upload_fail_put(
|
||||
async def test_agents_upload_fail_cloud(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
hass_storage: dict[str, Any],
|
||||
mock_get_upload_details: Mock,
|
||||
side_effect: Exception,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test agent upload backup, when cloud user is logged in."""
|
||||
client = await hass_client()
|
||||
@ -450,7 +458,7 @@ async def test_agents_upload_fail_cloud(
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
protected=True,
|
||||
size=0.0,
|
||||
size=0,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
@ -468,15 +476,20 @@ async def test_agents_upload_fail_cloud(
|
||||
"/api/backup/upload?agent_id=cloud.cloud",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert resp.status == 201
|
||||
assert "Error during backup upload - Failed to get upload details" in caplog.text
|
||||
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
|
||||
assert len(store_backups) == 1
|
||||
stored_backup = store_backups[0]
|
||||
assert stored_backup["backup_id"] == backup_id
|
||||
assert stored_backup["failed_agent_ids"] == ["cloud.cloud"]
|
||||
|
||||
|
||||
async def test_agents_upload_not_protected(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test agent upload backup, when cloud user is logged in."""
|
||||
client = await hass_client()
|
||||
@ -492,7 +505,7 @@ async def test_agents_upload_not_protected(
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
protected=False,
|
||||
size=0.0,
|
||||
size=0,
|
||||
)
|
||||
with (
|
||||
patch("pathlib.Path.open"),
|
||||
@ -505,9 +518,14 @@ async def test_agents_upload_not_protected(
|
||||
"/api/backup/upload?agent_id=cloud.cloud",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert resp.status == 201
|
||||
assert "Error during backup upload - Cloud backups must be protected" in caplog.text
|
||||
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
|
||||
assert len(store_backups) == 1
|
||||
stored_backup = store_backups[0]
|
||||
assert stored_backup["backup_id"] == backup_id
|
||||
assert stored_backup["failed_agent_ids"] == ["cloud.cloud"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
|
||||
|
@ -16,6 +16,7 @@ from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||
|
||||
from aiohasupervisor.exceptions import (
|
||||
SupervisorBadRequestError,
|
||||
SupervisorError,
|
||||
SupervisorNotFoundError,
|
||||
)
|
||||
from aiohasupervisor.models import (
|
||||
@ -46,7 +47,7 @@ TEST_BACKUP = supervisor_backups.Backup(
|
||||
compressed=False,
|
||||
content=supervisor_backups.BackupContent(
|
||||
addons=["ssl"],
|
||||
folders=["share"],
|
||||
folders=[supervisor_backups.Folder.SHARE],
|
||||
homeassistant=True,
|
||||
),
|
||||
date=datetime.fromisoformat("1970-01-01T00:00:00Z"),
|
||||
@ -71,7 +72,7 @@ TEST_BACKUP_DETAILS = supervisor_backups.BackupComplete(
|
||||
compressed=TEST_BACKUP.compressed,
|
||||
date=TEST_BACKUP.date,
|
||||
extra=None,
|
||||
folders=["share"],
|
||||
folders=[supervisor_backups.Folder.SHARE],
|
||||
homeassistant_exclude_database=False,
|
||||
homeassistant="2024.12.0",
|
||||
location=TEST_BACKUP.location,
|
||||
@ -197,7 +198,7 @@ async def hassio_enabled(
|
||||
@pytest.fixture
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant, hassio_enabled: None, supervisor_client: AsyncMock
|
||||
) -> AsyncGenerator[None]:
|
||||
) -> None:
|
||||
"""Set up Backup integration."""
|
||||
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
@ -451,7 +452,7 @@ async def test_agent_upload(
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
protected=False,
|
||||
size=0.0,
|
||||
size=0,
|
||||
)
|
||||
|
||||
supervisor_client.backups.reload.assert_not_called()
|
||||
@ -732,6 +733,292 @@ async def test_reader_writer_create(
|
||||
supervisor_client.backups.download_backup.assert_not_called()
|
||||
supervisor_client.backups.remove_backup.assert_not_called()
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {"manager_state": "idle"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_client", "setup_integration")
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_code", "error_message"),
|
||||
[
|
||||
(
|
||||
SupervisorError("Boom!"),
|
||||
"home_assistant_error",
|
||||
"Error creating backup: Boom!",
|
||||
),
|
||||
(Exception("Boom!"), "unknown_error", "Unknown error"),
|
||||
],
|
||||
)
|
||||
async def test_reader_writer_create_partial_backup_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
supervisor_client: AsyncMock,
|
||||
side_effect: Exception,
|
||||
error_code: str,
|
||||
error_message: str,
|
||||
) -> None:
|
||||
"""Test client partial backup error when generating a backup."""
|
||||
client = await hass_ws_client(hass)
|
||||
supervisor_client.backups.partial_backup.side_effect = side_effect
|
||||
|
||||
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/generate", "agent_ids": ["hassio.local"], "name": "Test"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "failed",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {"manager_state": "idle"}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == error_code
|
||||
assert response["error"]["message"] == error_message
|
||||
|
||||
assert supervisor_client.backups.partial_backup.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_client", "setup_integration")
|
||||
async def test_reader_writer_create_missing_reference_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test missing reference error when generating a backup."""
|
||||
client = await hass_ws_client(hass)
|
||||
supervisor_client.backups.partial_backup.return_value.job_id = "abc123"
|
||||
|
||||
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/generate", "agent_ids": ["hassio.local"], "name": "Test"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {"backup_job_id": "abc123"}
|
||||
|
||||
assert supervisor_client.backups.partial_backup.call_count == 1
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "job",
|
||||
"data": {"done": True, "uuid": "abc123"},
|
||||
},
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "failed",
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert supervisor_client.backups.backup_info.call_count == 0
|
||||
assert supervisor_client.backups.download_backup.call_count == 0
|
||||
assert supervisor_client.backups.remove_backup.call_count == 0
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {"manager_state": "idle"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_client", "setup_integration")
|
||||
@pytest.mark.parametrize("exception", [SupervisorError("Boom!"), Exception("Boom!")])
|
||||
@pytest.mark.parametrize(
|
||||
("method", "download_call_count", "remove_call_count"),
|
||||
[("download_backup", 1, 1), ("remove_backup", 1, 1)],
|
||||
)
|
||||
async def test_reader_writer_create_download_remove_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
supervisor_client: AsyncMock,
|
||||
exception: Exception,
|
||||
method: str,
|
||||
download_call_count: int,
|
||||
remove_call_count: int,
|
||||
) -> None:
|
||||
"""Test download and remove error when generating a backup."""
|
||||
client = await hass_ws_client(hass)
|
||||
supervisor_client.backups.partial_backup.return_value.job_id = "abc123"
|
||||
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
|
||||
method_mock = getattr(supervisor_client.backups, method)
|
||||
method_mock.side_effect = exception
|
||||
|
||||
remote_agent = BackupAgentTest("remote")
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
|
||||
spec_set=BackupAgentPlatformProtocol,
|
||||
),
|
||||
)
|
||||
|
||||
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/generate", "agent_ids": ["test.remote"], "name": "Test"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {"backup_job_id": "abc123"}
|
||||
|
||||
assert supervisor_client.backups.partial_backup.call_count == 1
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "job",
|
||||
"data": {"done": True, "uuid": "abc123", "reference": "test_slug"},
|
||||
},
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": "upload_to_agents",
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "failed",
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert supervisor_client.backups.backup_info.call_count == 1
|
||||
assert supervisor_client.backups.download_backup.call_count == download_call_count
|
||||
assert supervisor_client.backups.remove_backup.call_count == remove_call_count
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {"manager_state": "idle"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_client", "setup_integration")
|
||||
@pytest.mark.parametrize("exception", [SupervisorError("Boom!"), Exception("Boom!")])
|
||||
async def test_reader_writer_create_info_error(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
supervisor_client: AsyncMock,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test backup info error when generating a backup."""
|
||||
client = await hass_ws_client(hass)
|
||||
supervisor_client.backups.partial_backup.return_value.job_id = "abc123"
|
||||
supervisor_client.backups.backup_info.side_effect = exception
|
||||
|
||||
remote_agent = BackupAgentTest("remote")
|
||||
await _setup_backup_platform(
|
||||
hass,
|
||||
domain="test",
|
||||
platform=Mock(
|
||||
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
|
||||
spec_set=BackupAgentPlatformProtocol,
|
||||
),
|
||||
)
|
||||
|
||||
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/generate", "agent_ids": ["test.remote"], "name": "Test"}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "in_progress",
|
||||
}
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == {"backup_job_id": "abc123"}
|
||||
|
||||
assert supervisor_client.backups.partial_backup.call_count == 1
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "job",
|
||||
"data": {"done": True, "uuid": "abc123", "reference": "test_slug"},
|
||||
},
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {
|
||||
"manager_state": "create_backup",
|
||||
"stage": None,
|
||||
"state": "failed",
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert supervisor_client.backups.backup_info.call_count == 1
|
||||
assert supervisor_client.backups.download_backup.call_count == 0
|
||||
assert supervisor_client.backups.remove_backup.call_count == 0
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response["event"] == {"manager_state": "idle"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_client", "setup_integration")
|
||||
async def test_reader_writer_create_remote_backup(
|
||||
|
Loading…
x
Reference in New Issue
Block a user