mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 17:31:15 -04:00
MVP implementation of Backup sync agents (#126122)
* init sync agent * add syncing * root import * rename list to info and add sync state * Add base backup class * Revert unneded change * adjust tests * move to kitchen_sink * split * move * Adjustments * Adjustment * update * Tests * Test unknown agent * adjust * Adjust for different test environments * Change /info WS to contain a dictinary * reorder * Add websocket command to trigger sync from the supervisor * cleanup * Make mypy happier --------- Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
@@ -5,18 +5,25 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_MANAGER, DOMAIN, LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .http import async_register_http_views
|
||||
from .manager import BackupManager
|
||||
from .models import BackupSyncMetadata
|
||||
from .sync_agent import BackupSyncAgent, SyncedBackup
|
||||
from .websocket import async_register_websocket_handlers
|
||||
|
||||
__all__ = [
|
||||
"BackupSyncAgent",
|
||||
"BackupSyncMetadata",
|
||||
"SyncedBackup",
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Backup integration."""
|
||||
backup_manager = BackupManager(hass)
|
||||
hass.data[DATA_MANAGER] = backup_manager
|
||||
hass.data[DOMAIN] = backup_manager = BackupManager(hass)
|
||||
|
||||
with_hassio = is_hassio(hass)
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ from typing import TYPE_CHECKING
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import BackupManager
|
||||
from .manager import BaseBackupManager
|
||||
from .models import BaseBackup
|
||||
|
||||
DOMAIN = "backup"
|
||||
DATA_MANAGER: HassKey[BackupManager] = HassKey(DOMAIN)
|
||||
DATA_MANAGER: HassKey[BaseBackupManager[BaseBackup]] = HassKey(DOMAIN)
|
||||
LOGGER = getLogger(__package__)
|
||||
|
||||
EXCLUDE_FROM_BACKUP = [
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import DATA_MANAGER
|
||||
from .manager import BackupManager
|
||||
|
||||
|
||||
@callback
|
||||
@@ -39,7 +40,7 @@ class DownloadBackupView(HomeAssistantView):
|
||||
if not request["hass_user"].is_admin:
|
||||
return Response(status=HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
manager = request.app[KEY_HASS].data[DATA_MANAGER]
|
||||
manager = cast(BackupManager, request.app[KEY_HASS].data[DATA_MANAGER])
|
||||
backup = await manager.async_get_backup(slug=slug)
|
||||
|
||||
if backup is None or not backup.path.exists():
|
||||
|
||||
@@ -16,10 +16,11 @@ import tarfile
|
||||
from tarfile import TarError
|
||||
from tempfile import TemporaryDirectory
|
||||
import time
|
||||
from typing import Any, Protocol, cast
|
||||
from typing import Any, Generic, Protocol, cast
|
||||
|
||||
import aiohttp
|
||||
from securetar import SecureTarFile, atomic_contents_add
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from homeassistant.backup_restore import RESTORE_BACKUP_FILE
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
@@ -31,9 +32,13 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.json import json_loads_object
|
||||
|
||||
from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
|
||||
from .models import BaseBackup
|
||||
from .sync_agent import BackupPlatformAgentProtocol, BackupSyncAgent
|
||||
|
||||
BUF_SIZE = 2**20 * 4 # 4MB
|
||||
|
||||
_BackupT = TypeVar("_BackupT", bound=BaseBackup, default=BaseBackup)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class NewBackup:
|
||||
@@ -43,14 +48,10 @@ class NewBackup:
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Backup:
|
||||
class Backup(BaseBackup):
|
||||
"""Backup class."""
|
||||
|
||||
slug: str
|
||||
name: str
|
||||
date: str
|
||||
path: Path
|
||||
size: float
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
"""Return a dict representation of this backup."""
|
||||
@@ -76,19 +77,21 @@ class BackupPlatformProtocol(Protocol):
|
||||
"""Perform operations after a backup finishes."""
|
||||
|
||||
|
||||
class BaseBackupManager(abc.ABC):
|
||||
class BaseBackupManager(abc.ABC, Generic[_BackupT]):
|
||||
"""Define the format that backup managers can have."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the backup manager."""
|
||||
self.hass = hass
|
||||
self.backup_task: asyncio.Task | None = None
|
||||
self.backups: dict[str, Backup] = {}
|
||||
self.backups: dict[str, _BackupT] = {}
|
||||
self.loaded_platforms = False
|
||||
self.platforms: dict[str, BackupPlatformProtocol] = {}
|
||||
self.sync_agents: dict[str, BackupSyncAgent] = {}
|
||||
self.syncing = False
|
||||
|
||||
@callback
|
||||
def _add_platform(
|
||||
def _add_platform_pre_post_handlers(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
integration_domain: str,
|
||||
@@ -98,13 +101,25 @@ class BaseBackupManager(abc.ABC):
|
||||
if not hasattr(platform, "async_pre_backup") or not hasattr(
|
||||
platform, "async_post_backup"
|
||||
):
|
||||
LOGGER.warning(
|
||||
"%s does not implement required functions for the backup platform",
|
||||
integration_domain,
|
||||
)
|
||||
return
|
||||
|
||||
self.platforms[integration_domain] = platform
|
||||
|
||||
async def _async_add_platform_agents(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
integration_domain: str,
|
||||
platform: BackupPlatformAgentProtocol,
|
||||
) -> None:
|
||||
"""Add a platform to the backup manager."""
|
||||
if not hasattr(platform, "async_get_backup_sync_agents"):
|
||||
return
|
||||
|
||||
agents = await platform.async_get_backup_sync_agents(hass=hass)
|
||||
self.sync_agents.update(
|
||||
{f"{integration_domain}.{agent.name}": agent for agent in agents}
|
||||
)
|
||||
|
||||
async def async_pre_backup_actions(self, **kwargs: Any) -> None:
|
||||
"""Perform pre backup actions."""
|
||||
if not self.loaded_platforms:
|
||||
@@ -139,10 +154,22 @@ class BaseBackupManager(abc.ABC):
|
||||
|
||||
async def load_platforms(self) -> None:
|
||||
"""Load backup platforms."""
|
||||
if self.loaded_platforms:
|
||||
return
|
||||
await integration_platform.async_process_integration_platforms(
|
||||
self.hass, DOMAIN, self._add_platform, wait_for_platforms=True
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self._add_platform_pre_post_handlers,
|
||||
wait_for_platforms=True,
|
||||
)
|
||||
await integration_platform.async_process_integration_platforms(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self._async_add_platform_agents,
|
||||
wait_for_platforms=True,
|
||||
)
|
||||
LOGGER.debug("Loaded %s platforms", len(self.platforms))
|
||||
LOGGER.debug("Loaded %s agents", len(self.sync_agents))
|
||||
self.loaded_platforms = True
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -159,14 +186,14 @@ class BaseBackupManager(abc.ABC):
|
||||
"""Generate a backup."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_get_backups(self, **kwargs: Any) -> dict[str, Backup]:
|
||||
async def async_get_backups(self, **kwargs: Any) -> dict[str, _BackupT]:
|
||||
"""Get backups.
|
||||
|
||||
Return a dictionary of Backup instances keyed by their slug.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_get_backup(self, *, slug: str, **kwargs: Any) -> Backup | None:
|
||||
async def async_get_backup(self, *, slug: str, **kwargs: Any) -> _BackupT | None:
|
||||
"""Get a backup."""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -182,8 +209,12 @@ class BaseBackupManager(abc.ABC):
|
||||
) -> None:
|
||||
"""Receive and store a backup file from upload."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_sync_backup(self, *, slug: str, **kwargs: Any) -> None:
|
||||
"""Sync a backup."""
|
||||
|
||||
class BackupManager(BaseBackupManager):
|
||||
|
||||
class BackupManager(BaseBackupManager[Backup]):
|
||||
"""Backup manager for the Backup integration."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
@@ -192,10 +223,42 @@ class BackupManager(BaseBackupManager):
|
||||
self.backup_dir = Path(hass.config.path("backups"))
|
||||
self.loaded_backups = False
|
||||
|
||||
async def async_sync_backup(self, *, slug: str, **kwargs: Any) -> None:
|
||||
"""Sync a backup."""
|
||||
await self.load_platforms()
|
||||
|
||||
if not self.sync_agents:
|
||||
return
|
||||
|
||||
if not (backup := await self.async_get_backup(slug=slug)):
|
||||
return
|
||||
|
||||
self.syncing = True
|
||||
sync_backup_results = await asyncio.gather(
|
||||
*(
|
||||
agent.async_upload_backup(
|
||||
path=backup.path,
|
||||
metadata={
|
||||
"homeassistant": HAVERSION,
|
||||
"size": backup.size,
|
||||
"date": backup.date,
|
||||
"slug": backup.slug,
|
||||
"name": backup.name,
|
||||
},
|
||||
)
|
||||
for agent in self.sync_agents.values()
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
for result in sync_backup_results:
|
||||
if isinstance(result, Exception):
|
||||
LOGGER.error("Error during backup sync - %s", result)
|
||||
self.syncing = False
|
||||
|
||||
async def load_backups(self) -> None:
|
||||
"""Load data of stored backup files."""
|
||||
backups = await self.hass.async_add_executor_job(self._read_backups)
|
||||
LOGGER.debug("Loaded %s backups", len(backups))
|
||||
LOGGER.debug("Loaded %s local backups", len(backups))
|
||||
self.backups = backups
|
||||
self.loaded_backups = True
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Models for the backup integration."""
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
@dataclass()
|
||||
class BaseBackup:
|
||||
"""Base backup class."""
|
||||
|
||||
date: str
|
||||
slug: str
|
||||
size: float
|
||||
name: str
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
"""Return a dict representation of this backup."""
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class BackupSyncMetadata(TypedDict):
|
||||
"""Dictionary type for backup sync metadata."""
|
||||
|
||||
date: str # The date the backup was created
|
||||
slug: str # The slug of the backup
|
||||
size: float # The size of the backup (in bytes)
|
||||
name: str # The name of the backup
|
||||
homeassistant: str # The version of Home Assistant that created the backup
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Backup sync agents for the Backup integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .models import BackupSyncMetadata, BaseBackup
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SyncedBackup(BaseBackup):
|
||||
"""Synced backup class."""
|
||||
|
||||
id: str
|
||||
|
||||
|
||||
class BackupSyncAgent(abc.ABC):
|
||||
"""Define the format that backup sync agents can have."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialize the backup sync agent."""
|
||||
self.name = name
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_download_backup(
|
||||
self,
|
||||
*,
|
||||
id: str,
|
||||
path: Path,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Download a backup file.
|
||||
|
||||
The `id` parameter is the ID of the synced backup that was returned in async_list_backups.
|
||||
|
||||
The `path` parameter is the full file path to download the synced backup to.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
path: Path,
|
||||
metadata: BackupSyncMetadata,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Upload a backup.
|
||||
|
||||
The `path` parameter is the full file path to the backup that should be synced.
|
||||
|
||||
The `metadata` parameter contains metadata about the backup that should be synced.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[SyncedBackup]:
|
||||
"""List backups."""
|
||||
|
||||
|
||||
class BackupPlatformAgentProtocol(Protocol):
|
||||
"""Define the format that backup platforms can have."""
|
||||
|
||||
async def async_get_backup_sync_agents(
|
||||
self,
|
||||
*,
|
||||
hass: HomeAssistant,
|
||||
**kwargs: Any,
|
||||
) -> list[BackupSyncAgent]:
|
||||
"""Register the backup sync agent."""
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Websocket commands for the Backup integration."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -14,9 +15,14 @@ from .manager import BackupProgress
|
||||
@callback
|
||||
def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) -> None:
|
||||
"""Register websocket commands."""
|
||||
websocket_api.async_register_command(hass, backup_agents_download)
|
||||
websocket_api.async_register_command(hass, backup_agents_info)
|
||||
websocket_api.async_register_command(hass, backup_agents_list_synced_backups)
|
||||
|
||||
if with_hassio:
|
||||
websocket_api.async_register_command(hass, handle_backup_end)
|
||||
websocket_api.async_register_command(hass, handle_backup_start)
|
||||
websocket_api.async_register_command(hass, handle_backup_sync)
|
||||
return
|
||||
|
||||
websocket_api.async_register_command(hass, handle_details)
|
||||
@@ -40,7 +46,7 @@ async def handle_info(
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"backups": list(backups.values()),
|
||||
"backups": [b.as_dict() for b in backups.values()],
|
||||
"backing_up": manager.backup_task is not None,
|
||||
},
|
||||
)
|
||||
@@ -162,3 +168,105 @@ async def handle_backup_end(
|
||||
return
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.ws_require_user(only_supervisor=True)
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "backup/sync",
|
||||
vol.Required("data"): {
|
||||
vol.Required("slug"): str,
|
||||
},
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def handle_backup_sync(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Backup sync notification."""
|
||||
LOGGER.debug("Backup sync notification")
|
||||
data = msg["data"]
|
||||
|
||||
try:
|
||||
await hass.data[DATA_MANAGER].async_sync_backup(slug=data["slug"])
|
||||
except Exception as err: # noqa: BLE001
|
||||
connection.send_error(msg["id"], "backup_sync_failed", str(err))
|
||||
return
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "backup/agents/info"})
|
||||
@websocket_api.async_response
|
||||
async def backup_agents_info(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return backup agents info."""
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
await manager.load_platforms()
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"agents": [{"id": agent_id} for agent_id in manager.sync_agents],
|
||||
"syncing": manager.syncing,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "backup/agents/synced"})
|
||||
@websocket_api.async_response
|
||||
async def backup_agents_list_synced_backups(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return a list of synced backups."""
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
backups: list[dict[str, Any]] = []
|
||||
await manager.load_platforms()
|
||||
for agent_id, agent in manager.sync_agents.items():
|
||||
_listed_backups = await agent.async_list_backups()
|
||||
backups.extend({**b.as_dict(), "agent_id": agent_id} for b in _listed_backups)
|
||||
connection.send_result(msg["id"], backups)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "backup/agents/download",
|
||||
vol.Required("agent"): str,
|
||||
vol.Required("sync_id"): str,
|
||||
vol.Required("slug"): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def backup_agents_download(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Download a synced backup."""
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
await manager.load_platforms()
|
||||
|
||||
if not (agent := manager.sync_agents.get(msg["agent"])):
|
||||
connection.send_error(
|
||||
msg["id"], "unknown_agent", f"Agent {msg['agent']} not found"
|
||||
)
|
||||
return
|
||||
try:
|
||||
await agent.async_download_backup(
|
||||
id=msg["sync_id"],
|
||||
path=Path(hass.config.path("backup"), f"{msg['slug']}.tar"),
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
connection.send_error(msg["id"], "backup_agents_download", str(err))
|
||||
return
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Backup platform for the kitchen_sink integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
BackupSyncAgent,
|
||||
BackupSyncMetadata,
|
||||
SyncedBackup,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_get_backup_sync_agents(
|
||||
hass: HomeAssistant,
|
||||
) -> list[BackupSyncAgent]:
|
||||
"""Register the backup sync agents."""
|
||||
return [KitchenSinkBackupSyncAgent("syncer")]
|
||||
|
||||
|
||||
class KitchenSinkBackupSyncAgent(BackupSyncAgent):
|
||||
"""Kitchen sink backup sync agent."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialize the kitchen sink backup sync agent."""
|
||||
super().__init__(name)
|
||||
self._uploads = [
|
||||
SyncedBackup(
|
||||
id="def456",
|
||||
name="Kitchen sink syncer",
|
||||
slug="abc123",
|
||||
size=1234,
|
||||
date="1970-01-01T00:00:00Z",
|
||||
)
|
||||
]
|
||||
|
||||
async def async_download_backup(
|
||||
self,
|
||||
*,
|
||||
id: str,
|
||||
path: Path,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Download a backup file."""
|
||||
LOGGER.info("Downloading backup %s to %s", id, path)
|
||||
|
||||
async def async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
path: Path,
|
||||
metadata: BackupSyncMetadata,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Upload a backup."""
|
||||
LOGGER.info("Uploading backup %s %s", path.name, metadata)
|
||||
self._uploads.append(
|
||||
SyncedBackup(
|
||||
id=uuid4().hex,
|
||||
name=metadata["name"],
|
||||
slug=metadata["slug"],
|
||||
size=metadata["size"],
|
||||
date=metadata["date"],
|
||||
)
|
||||
)
|
||||
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[SyncedBackup]:
|
||||
"""List synced backups."""
|
||||
return self._uploads
|
||||
@@ -3,10 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.backup import DOMAIN
|
||||
from homeassistant.components.backup.manager import Backup
|
||||
from homeassistant.components.backup.models import BackupSyncMetadata
|
||||
from homeassistant.components.backup.sync_agent import BackupSyncAgent, SyncedBackup
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -20,6 +23,40 @@ TEST_BACKUP = Backup(
|
||||
)
|
||||
|
||||
|
||||
class BackupSyncAgentTest(BackupSyncAgent):
|
||||
"""Test backup sync agent."""
|
||||
|
||||
async def async_download_backup(
|
||||
self,
|
||||
*,
|
||||
id: str,
|
||||
path: Path,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Download a backup file."""
|
||||
|
||||
async def async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
path: Path,
|
||||
metadata: BackupSyncMetadata,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Upload a backup."""
|
||||
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[SyncedBackup]:
|
||||
"""List synced backups."""
|
||||
return [
|
||||
SyncedBackup(
|
||||
id="abc123",
|
||||
name="Test",
|
||||
slug="abc123",
|
||||
size=13.37,
|
||||
date="1970-01-01T00:00:00Z",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def setup_backup_integration(
|
||||
hass: HomeAssistant,
|
||||
with_hassio: bool = False,
|
||||
|
||||
@@ -1,4 +1,106 @@
|
||||
# serializer version: 1
|
||||
# name: test_agents_download[with_hassio]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': None,
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_agents_download[without_hassio]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': None,
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_agents_download_exception
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'backup_agents_download',
|
||||
'message': 'Boom',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_agents_download_unknown_agent
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'unknown_agent',
|
||||
'message': 'Agent domain.test not found',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_agents_info[with_hassio]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': dict({
|
||||
'agents': list([
|
||||
dict({
|
||||
'id': 'domain.test',
|
||||
}),
|
||||
]),
|
||||
'syncing': False,
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_agents_info[without_hassio]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': dict({
|
||||
'agents': list([
|
||||
dict({
|
||||
'id': 'domain.test',
|
||||
}),
|
||||
]),
|
||||
'syncing': False,
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_agents_synced[with_hassio]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': list([
|
||||
dict({
|
||||
'agent_id': 'domain.test',
|
||||
'date': '1970-01-01T00:00:00Z',
|
||||
'id': 'abc123',
|
||||
'name': 'Test',
|
||||
'size': 13.37,
|
||||
'slug': 'abc123',
|
||||
}),
|
||||
]),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_agents_synced[without_hassio]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': list([
|
||||
dict({
|
||||
'agent_id': 'domain.test',
|
||||
'date': '1970-01-01T00:00:00Z',
|
||||
'id': 'abc123',
|
||||
'name': 'Test',
|
||||
'size': 13.37,
|
||||
'slug': 'abc123',
|
||||
}),
|
||||
]),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_end[with_hassio-hass_access_token]
|
||||
dict({
|
||||
'error': dict({
|
||||
@@ -147,6 +249,80 @@
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_sync[with_hassio-hass_access_token]
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'only_supervisor',
|
||||
'message': 'Only allowed as Supervisor',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_sync[with_hassio-hass_supervisor_access_token]
|
||||
dict({
|
||||
'id': 1,
|
||||
'result': None,
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_sync[without_hassio-hass_access_token]
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'unknown_command',
|
||||
'message': 'Unknown command.',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_sync[without_hassio-hass_supervisor_access_token]
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'unknown_command',
|
||||
'message': 'Unknown command.',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_sync_excepion[exception0]
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'backup_sync_failed',
|
||||
'message': '',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_sync_excepion[exception1]
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'backup_sync_failed',
|
||||
'message': 'Boom',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_backup_sync_excepion[exception2]
|
||||
dict({
|
||||
'error': dict({
|
||||
'code': 'backup_sync_failed',
|
||||
'message': 'Boom',
|
||||
}),
|
||||
'id': 1,
|
||||
'success': False,
|
||||
'type': 'result',
|
||||
})
|
||||
# ---
|
||||
# name: test_details[with_hassio-with_backup_content]
|
||||
dict({
|
||||
'error': dict({
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch
|
||||
|
||||
import aiohttp
|
||||
@@ -14,11 +15,12 @@ from homeassistant.components.backup.manager import (
|
||||
BackupPlatformProtocol,
|
||||
BackupProgress,
|
||||
)
|
||||
from homeassistant.components.backup.sync_agent import BackupPlatformAgentProtocol
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import TEST_BACKUP
|
||||
from .common import TEST_BACKUP, BackupSyncAgentTest
|
||||
|
||||
from tests.common import MockPlatform, mock_platform
|
||||
|
||||
@@ -39,7 +41,7 @@ async def _mock_backup_generation(
|
||||
assert manager.backup_task is not None
|
||||
assert progress == []
|
||||
|
||||
await manager.backup_task
|
||||
backup = await manager.backup_task
|
||||
assert progress == [BackupProgress(done=True, stage=None, success=True)]
|
||||
|
||||
assert mocked_json_bytes.call_count == 1
|
||||
@@ -48,10 +50,12 @@ async def _mock_backup_generation(
|
||||
assert backup_json_dict["homeassistant"] == {"version": "2025.1.0"}
|
||||
assert manager.backup_dir.as_posix() in str(mocked_tarfile.call_args_list[0][0][0])
|
||||
|
||||
return backup
|
||||
|
||||
|
||||
async def _setup_mock_domain(
|
||||
hass: HomeAssistant,
|
||||
platform: BackupPlatformProtocol | None = None,
|
||||
platform: BackupPlatformProtocol | BackupPlatformAgentProtocol | None = None,
|
||||
) -> None:
|
||||
"""Set up a mock domain."""
|
||||
mock_platform(hass, "some_domain.backup", platform or MockPlatform())
|
||||
@@ -174,6 +178,7 @@ async def test_async_create_backup(
|
||||
assert "Generated new backup with slug " in caplog.text
|
||||
assert "Creating backup directory" in caplog.text
|
||||
assert "Loaded 0 platforms" in caplog.text
|
||||
assert "Loaded 0 agents" in caplog.text
|
||||
|
||||
|
||||
async def test_loading_platforms(
|
||||
@@ -191,6 +196,7 @@ async def test_loading_platforms(
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_sync_agents=AsyncMock(),
|
||||
),
|
||||
)
|
||||
await manager.load_platforms()
|
||||
@@ -202,6 +208,34 @@ async def test_loading_platforms(
|
||||
assert "Loaded 1 platforms" in caplog.text
|
||||
|
||||
|
||||
async def test_loading_sync_agents(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test loading backup sync agents."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
assert not manager.loaded_platforms
|
||||
assert not manager.platforms
|
||||
|
||||
await _setup_mock_domain(
|
||||
hass,
|
||||
Mock(
|
||||
async_get_backup_sync_agents=AsyncMock(
|
||||
return_value=[BackupSyncAgentTest("test")]
|
||||
),
|
||||
),
|
||||
)
|
||||
await manager.load_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert manager.loaded_platforms
|
||||
assert len(manager.sync_agents) == 1
|
||||
|
||||
assert "Loaded 1 agents" in caplog.text
|
||||
assert "some_domain.test" in manager.sync_agents
|
||||
|
||||
|
||||
async def test_not_loading_bad_platforms(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -220,10 +254,151 @@ async def test_not_loading_bad_platforms(
|
||||
assert len(manager.platforms) == 0
|
||||
|
||||
assert "Loaded 0 platforms" in caplog.text
|
||||
assert (
|
||||
"some_domain does not implement required functions for the backup platform"
|
||||
in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
async def test_syncing_backup(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mocked_json_bytes: Mock,
|
||||
mocked_tarfile: Mock,
|
||||
) -> None:
|
||||
"""Test syncing a backup."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
await _setup_mock_domain(
|
||||
hass,
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_sync_agents=AsyncMock(
|
||||
return_value=[
|
||||
BackupSyncAgentTest("agent1"),
|
||||
BackupSyncAgentTest("agent2"),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
await manager.load_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
backup = await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
|
||||
return_value=backup,
|
||||
),
|
||||
patch.object(BackupSyncAgentTest, "async_upload_backup") as mocked_upload,
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.HAVERSION",
|
||||
"2025.1.0",
|
||||
),
|
||||
):
|
||||
await manager.async_sync_backup(slug=backup.slug)
|
||||
assert mocked_upload.call_count == 2
|
||||
first_call = mocked_upload.call_args_list[0]
|
||||
assert first_call[1]["path"] == backup.path
|
||||
assert first_call[1]["metadata"] == {
|
||||
"date": backup.date,
|
||||
"homeassistant": "2025.1.0",
|
||||
"name": backup.name,
|
||||
"size": backup.size,
|
||||
"slug": backup.slug,
|
||||
}
|
||||
|
||||
assert "Error during backup sync" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
async def test_syncing_backup_with_exception(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mocked_json_bytes: Mock,
|
||||
mocked_tarfile: Mock,
|
||||
) -> None:
|
||||
"""Test syncing a backup with exception."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
class ModifiedBackupSyncAgentTest(BackupSyncAgentTest):
|
||||
async def async_upload_backup(self, **kwargs: Any) -> None:
|
||||
raise HomeAssistantError("Test exception")
|
||||
|
||||
await _setup_mock_domain(
|
||||
hass,
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_sync_agents=AsyncMock(
|
||||
return_value=[
|
||||
ModifiedBackupSyncAgentTest("agent1"),
|
||||
ModifiedBackupSyncAgentTest("agent2"),
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
await manager.load_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
backup = await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
|
||||
return_value=backup,
|
||||
),
|
||||
patch.object(
|
||||
ModifiedBackupSyncAgentTest,
|
||||
"async_upload_backup",
|
||||
) as mocked_upload,
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.HAVERSION",
|
||||
"2025.1.0",
|
||||
),
|
||||
):
|
||||
mocked_upload.side_effect = HomeAssistantError("Test exception")
|
||||
await manager.async_sync_backup(slug=backup.slug)
|
||||
assert mocked_upload.call_count == 2
|
||||
first_call = mocked_upload.call_args_list[0]
|
||||
assert first_call[1]["path"] == backup.path
|
||||
assert first_call[1]["metadata"] == {
|
||||
"date": backup.date,
|
||||
"homeassistant": "2025.1.0",
|
||||
"name": backup.name,
|
||||
"size": backup.size,
|
||||
"slug": backup.slug,
|
||||
}
|
||||
|
||||
assert "Error during backup sync - Test exception" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_backup_generation")
|
||||
async def test_syncing_backup_no_agents(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mocked_json_bytes: Mock,
|
||||
mocked_tarfile: Mock,
|
||||
) -> None:
|
||||
"""Test syncing a backup with no agents."""
|
||||
manager = BackupManager(hass)
|
||||
|
||||
await _setup_mock_domain(
|
||||
hass,
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_sync_agents=AsyncMock(return_value=[]),
|
||||
),
|
||||
)
|
||||
await manager.load_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
backup = await _mock_backup_generation(manager, mocked_json_bytes, mocked_tarfile)
|
||||
with patch(
|
||||
"homeassistant.components.backup.sync_agent.BackupSyncAgent.async_upload_backup"
|
||||
) as mocked_async_upload_backup:
|
||||
await manager.async_sync_backup(slug=backup.slug)
|
||||
assert mocked_async_upload_backup.call_count == 0
|
||||
|
||||
|
||||
async def test_exception_plaform_pre(
|
||||
@@ -241,6 +416,7 @@ async def test_exception_plaform_pre(
|
||||
Mock(
|
||||
async_pre_backup=_mock_step,
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_sync_agents=AsyncMock(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -263,6 +439,7 @@ async def test_exception_plaform_post(
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=_mock_step,
|
||||
async_get_backup_sync_agents=AsyncMock(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -285,6 +462,7 @@ async def test_loading_platforms_when_running_async_pre_backup_actions(
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_sync_agents=AsyncMock(),
|
||||
),
|
||||
)
|
||||
await manager.async_pre_backup_actions()
|
||||
@@ -310,6 +488,7 @@ async def test_loading_platforms_when_running_async_post_backup_actions(
|
||||
Mock(
|
||||
async_pre_backup=AsyncMock(),
|
||||
async_post_backup=AsyncMock(),
|
||||
async_get_backup_sync_agents=AsyncMock(),
|
||||
),
|
||||
)
|
||||
await manager.async_post_backup_actions()
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
"""Tests for the Backup integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.backup.manager import Backup
|
||||
from homeassistant.components.backup.const import DATA_MANAGER
|
||||
from homeassistant.components.backup.models import BaseBackup
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .common import TEST_BACKUP, setup_backup_integration
|
||||
from .common import TEST_BACKUP, BackupSyncAgentTest, setup_backup_integration
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
@@ -43,15 +45,23 @@ async def test_info(
|
||||
"""Test getting backup info."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
|
||||
hass.data[DATA_MANAGER].backups = {TEST_BACKUP.slug: TEST_BACKUP}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backups",
|
||||
return_value={TEST_BACKUP.slug: TEST_BACKUP},
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.load_backups",
|
||||
AsyncMock(),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backups",
|
||||
return_value={TEST_BACKUP.slug: TEST_BACKUP},
|
||||
),
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/info"})
|
||||
assert snapshot == await client.receive_json()
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -73,7 +83,7 @@ async def test_details(
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
with_hassio: bool,
|
||||
backup_content: Backup | None,
|
||||
backup_content: BaseBackup | None,
|
||||
) -> None:
|
||||
"""Test getting backup info."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
@@ -112,7 +122,7 @@ async def test_remove(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_remove_backup",
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/remove", "slug": "abc123"})
|
||||
assert snapshot == await client.receive_json()
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -140,7 +150,7 @@ async def test_generate(
|
||||
|
||||
await client.send_json_auto_id({"type": "backup/generate"})
|
||||
for _ in range(number_of_messages):
|
||||
assert snapshot == await client.receive_json()
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -199,7 +209,7 @@ async def test_backup_end(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_post_backup_actions",
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/end"})
|
||||
assert snapshot == await client.receive_json()
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -232,7 +242,47 @@ async def test_backup_start(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_pre_backup_actions",
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/start"})
|
||||
assert snapshot == await client.receive_json()
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"access_token_fixture_name",
|
||||
["hass_access_token", "hass_supervisor_access_token"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("with_hassio"),
|
||||
[
|
||||
pytest.param(True, id="with_hassio"),
|
||||
pytest.param(False, id="without_hassio"),
|
||||
],
|
||||
)
|
||||
async def test_backup_sync(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
sync_access_token_proxy: str,
|
||||
*,
|
||||
access_token_fixture_name: str,
|
||||
with_hassio: bool,
|
||||
) -> None:
|
||||
"""Test handling of pre backup actions from a WS command."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
|
||||
client = await hass_ws_client(hass, sync_access_token_proxy)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_sync_backup",
|
||||
):
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "backup/sync",
|
||||
"data": {
|
||||
"slug": "abc123",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -261,7 +311,43 @@ async def test_backup_end_excepion(
|
||||
side_effect=exception,
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/end"})
|
||||
assert snapshot == await client.receive_json()
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
TimeoutError(),
|
||||
HomeAssistantError("Boom"),
|
||||
Exception("Boom"),
|
||||
],
|
||||
)
|
||||
async def test_backup_sync_excepion(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
hass_supervisor_access_token: str,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test exception handling while running sync backup action from a WS command."""
|
||||
await setup_backup_integration(hass, with_hassio=True)
|
||||
|
||||
client = await hass_ws_client(hass, hass_supervisor_access_token)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_sync_backup",
|
||||
side_effect=exception,
|
||||
):
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "backup/sync",
|
||||
"data": {
|
||||
"slug": "abc123",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -290,4 +376,135 @@ async def test_backup_start_excepion(
|
||||
side_effect=exception,
|
||||
):
|
||||
await client.send_json_auto_id({"type": "backup/start"})
|
||||
assert snapshot == await client.receive_json()
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"with_hassio",
|
||||
[
|
||||
pytest.param(True, id="with_hassio"),
|
||||
pytest.param(False, id="without_hassio"),
|
||||
],
|
||||
)
|
||||
async def test_agents_info(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
with_hassio: bool,
|
||||
) -> None:
|
||||
"""Test getting backup agents info."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
hass.data[DATA_MANAGER].sync_agents = {"domain.test": BackupSyncAgentTest("test")}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json_auto_id({"type": "backup/agents/info"})
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"with_hassio",
|
||||
[
|
||||
pytest.param(True, id="with_hassio"),
|
||||
pytest.param(False, id="without_hassio"),
|
||||
],
|
||||
)
|
||||
async def test_agents_synced(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
with_hassio: bool,
|
||||
) -> None:
|
||||
"""Test getting backup agents synced details."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
hass.data[DATA_MANAGER].sync_agents = {"domain.test": BackupSyncAgentTest("test")}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json_auto_id({"type": "backup/agents/synced"})
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"with_hassio",
|
||||
[
|
||||
pytest.param(True, id="with_hassio"),
|
||||
pytest.param(False, id="without_hassio"),
|
||||
],
|
||||
)
|
||||
async def test_agents_download(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
with_hassio: bool,
|
||||
) -> None:
|
||||
"""Test WS command to start downloading a synced backup."""
|
||||
await setup_backup_integration(hass, with_hassio=with_hassio)
|
||||
hass.data[DATA_MANAGER].sync_agents = {"domain.test": BackupSyncAgentTest("test")}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "backup/agents/download",
|
||||
"slug": "abc123",
|
||||
"agent": "domain.test",
|
||||
"sync_id": "abc123",
|
||||
}
|
||||
)
|
||||
with patch.object(BackupSyncAgentTest, "async_download_backup") as download_mock:
|
||||
assert await client.receive_json() == snapshot
|
||||
assert download_mock.call_args[1] == {
|
||||
"id": "abc123",
|
||||
"path": Path(hass.config.path("backup"), "abc123.tar"),
|
||||
}
|
||||
|
||||
|
||||
async def test_agents_download_exception(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test WS command to start downloading a synced backup throwing an exception."""
|
||||
await setup_backup_integration(hass)
|
||||
hass.data[DATA_MANAGER].sync_agents = {"domain.test": BackupSyncAgentTest("test")}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "backup/agents/download",
|
||||
"slug": "abc123",
|
||||
"agent": "domain.test",
|
||||
"sync_id": "abc123",
|
||||
}
|
||||
)
|
||||
with patch.object(BackupSyncAgentTest, "async_download_backup") as download_mock:
|
||||
download_mock.side_effect = Exception("Boom")
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
|
||||
async def test_agents_download_unknown_agent(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test downloading a synced backup with an unknown agent."""
|
||||
await setup_backup_integration(hass)
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "backup/agents/download",
|
||||
"slug": "abc123",
|
||||
"agent": "domain.test",
|
||||
"sync_id": "abc123",
|
||||
}
|
||||
)
|
||||
assert await client.receive_json() == snapshot
|
||||
|
||||
Reference in New Issue
Block a user