From 9a687e7f945870341b0ccc2c593713e23a28b728 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jan 2025 14:04:17 +0100 Subject: [PATCH] Add support for per-backup agent encryption flag (#136622) * Add support for per-backup agent encryption flag * Adjust * Don't attempt decrypting an unprotected backup * Address review comments * Add some tests * Add fixture * Rename fixture * Correct condition for when we should encrypt or decrypt * Update tests in integrations * Improve test coverage * Fix onedrive tests * Add test * Improve cipher worker shutdown * Improve test coverage * Fix google_drive tests * Move inner class _CipherBackupStreamer._WorkerStatus to module scope --- homeassistant/components/backup/config.py | 44 + homeassistant/components/backup/http.py | 6 +- homeassistant/components/backup/manager.py | 103 ++- homeassistant/components/backup/models.py | 20 +- homeassistant/components/backup/store.py | 4 +- homeassistant/components/backup/util.py | 205 +++- homeassistant/components/backup/websocket.py | 3 +- tests/components/backup/conftest.py | 1 + .../test_backups/c0cb53bd.tar.decrypted | Bin 0 -> 10240 bytes .../backup/snapshots/test_backup.ambr | 11 +- .../backup/snapshots/test_store.ambr | 14 + .../backup/snapshots/test_websocket.ambr | 873 +++++++++++++++--- tests/components/backup/test_manager.py | 265 +++++- tests/components/backup/test_store.py | 1 + tests/components/backup/test_util.py | 258 +++++- tests/components/backup/test_websocket.py | 270 ++++-- tests/components/cloud/test_backup.py | 8 +- tests/components/google_drive/test_backup.py | 4 +- tests/components/hassio/test_backup.py | 8 +- tests/components/kitchen_sink/test_backup.py | 8 +- tests/components/onedrive/test_backup.py | 15 +- tests/components/synology_dsm/test_backup.py | 18 +- 22 files changed, 1791 insertions(+), 348 deletions(-) create mode 100644 tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted diff --git a/homeassistant/components/backup/config.py b/homeassistant/components/backup/config.py index 1d1b8046360..0baefe1f52d 100644 --- a/homeassistant/components/backup/config.py +++ b/homeassistant/components/backup/config.py @@ -40,6 +40,7 @@ BACKUP_START_TIME_JITTER = 60 * 60 class StoredBackupConfig(TypedDict): """Represent the stored backup config.""" + agents: dict[str, StoredAgentConfig] create_backup: StoredCreateBackupConfig last_attempted_automatic_backup: str | None last_completed_automatic_backup: str | None @@ -51,6 +52,7 @@ class StoredBackupConfig(TypedDict): class BackupConfigData: """Represent loaded backup config data.""" + agents: dict[str, AgentConfig] create_backup: CreateBackupConfig last_attempted_automatic_backup: datetime | None = None last_completed_automatic_backup: datetime | None = None @@ -84,6 +86,10 @@ class BackupConfigData: days = [Day(day) for day in data["schedule"]["days"]] return cls( + agents={ + agent_id: AgentConfig(protected=agent_data["protected"]) + for agent_id, agent_data in data["agents"].items() + }, create_backup=CreateBackupConfig( agent_ids=data["create_backup"]["agent_ids"], include_addons=data["create_backup"]["include_addons"], @@ -120,6 +126,9 @@ class BackupConfigData: last_completed = None return StoredBackupConfig( + agents={ + agent_id: agent.to_dict() for agent_id, agent in self.agents.items() + }, create_backup=self.create_backup.to_dict(), last_attempted_automatic_backup=last_attempted, last_completed_automatic_backup=last_completed, @@ -134,6 +143,7 @@ class BackupConfig: def __init__(self, hass: HomeAssistant, manager: BackupManager) -> None: """Initialize backup config.""" self.data = BackupConfigData( + agents={}, create_backup=CreateBackupConfig(), retention=RetentionConfig(), schedule=BackupSchedule(), @@ -149,11 +159,20 @@ class BackupConfig: async def update( self, *, + agents: dict[str, AgentParametersDict] | UndefinedType = UNDEFINED, create_backup: CreateBackupParametersDict | UndefinedType = UNDEFINED, retention: RetentionParametersDict | UndefinedType = UNDEFINED, schedule: ScheduleParametersDict | UndefinedType = UNDEFINED, ) -> None: """Update config.""" + if agents is not UNDEFINED: + for agent_id, agent_config in agents.items(): + if agent_id not in self.data.agents: + self.data.agents[agent_id] = AgentConfig(**agent_config) + else: + self.data.agents[agent_id] = replace( + self.data.agents[agent_id], **agent_config + ) if create_backup is not UNDEFINED: self.data.create_backup = replace(self.data.create_backup, **create_backup) if retention is not UNDEFINED: @@ -170,6 +189,31 @@ class BackupConfig: self._manager.store.save() +@dataclass(kw_only=True) +class AgentConfig: + """Represent the config for an agent.""" + + protected: bool + + def to_dict(self) -> StoredAgentConfig: + """Convert agent config to a dict.""" + return { + "protected": self.protected, + } + + +class StoredAgentConfig(TypedDict): + """Represent the stored config for an agent.""" + + protected: bool + + +class AgentParametersDict(TypedDict, total=False): + """Represent the parameters for an agent.""" + + protected: bool + + @dataclass(kw_only=True) class RetentionConfig: """Represent the backup retention configuration.""" diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py index 3d3877cc2f7..6b06db4601d 100644 --- a/homeassistant/components/backup/http.py +++ b/homeassistant/components/backup/http.py @@ -69,7 +69,7 @@ class DownloadBackupView(HomeAssistantView): CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar" } - if not password: + if not password or not backup.protected: return await self._send_backup_no_password( request, headers, backup_id, agent_id, agent, manager ) @@ -123,13 +123,13 @@ class DownloadBackupView(HomeAssistantView): worker_done_event = asyncio.Event() - def on_done() -> None: + def on_done(error: Exception | None) -> None: """Call by the worker thread when it's done.""" hass.loop.call_soon_threadsafe(worker_done_event.set) stream = util.AsyncIteratorWriter(hass) worker = threading.Thread( - target=util.decrypt_backup, args=[reader, stream, password, on_done] + target=util.decrypt_backup, args=[reader, stream, password, on_done, 0, []] ) try: worker.start() diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 19ebb8011ee..1f439160381 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -5,7 +5,7 @@ from __future__ import annotations import abc import asyncio from collections.abc import AsyncIterator, Callable, Coroutine -from dataclasses import dataclass +from dataclasses import dataclass, replace from enum import StrEnum import hashlib import io @@ -46,10 +46,12 @@ from .const import ( EXCLUDE_FROM_BACKUP, LOGGER, ) -from .models import AgentBackup, BackupError, BackupManagerError, Folder +from .models import AgentBackup, BackupError, BackupManagerError, BaseBackup, Folder from .store import BackupStore from .util import ( AsyncIteratorReader, + DecryptedBackupStreamer, + EncryptedBackupStreamer, make_backup_dir, read_backup, validate_password, @@ -65,10 +67,18 @@ class NewBackup: @dataclass(frozen=True, kw_only=True, slots=True) -class ManagerBackup(AgentBackup): +class AgentBackupStatus: + """Agent specific backup attributes.""" + + protected: bool + size: int + + +@dataclass(frozen=True, kw_only=True, slots=True) +class ManagerBackup(BaseBackup): """Backup class.""" - agent_ids: list[str] + agents: dict[str, AgentBackupStatus] failed_agent_ids: list[str] with_automatic_settings: bool | None @@ -437,20 +447,61 @@ class BackupManager: backup: AgentBackup, agent_ids: list[str], open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]], + password: str | None, ) -> dict[str, Exception]: """Upload a backup to selected agents.""" agent_errors: dict[str, Exception] = {} LOGGER.debug("Uploading backup %s to agents %s", backup.backup_id, agent_ids) - sync_backup_results = await asyncio.gather( - *( - self.backup_agents[agent_id].async_upload_backup( - open_stream=open_stream, - backup=backup, + async def upload_backup_to_agent(agent_id: str) -> None: + """Upload backup to a single agent, and encrypt or decrypt as needed.""" + config = self.config.data.agents.get(agent_id) + should_encrypt = config.protected if config else password is not None + streamer: DecryptedBackupStreamer | EncryptedBackupStreamer | None = None + if should_encrypt == backup.protected or password is None: + # The backup we're uploading is already in the correct state, or we + # don't have a password to encrypt or decrypt it + LOGGER.debug( + "Uploading backup %s to agent %s as is", backup.backup_id, agent_id ) - for agent_id in agent_ids - ), + open_stream_func = open_stream + _backup = backup + elif should_encrypt: + # The backup we're uploading is not encrypted, but the agent requires it + LOGGER.debug( + "Uploading encrypted backup %s to agent %s", + backup.backup_id, + agent_id, + ) + streamer = EncryptedBackupStreamer( + self.hass, backup, open_stream, password + ) + else: + # The backup we're uploading is encrypted, but the agent requires it + # decrypted + LOGGER.debug( + "Uploading decrypted backup %s to agent %s", + backup.backup_id, + agent_id, + ) + streamer = DecryptedBackupStreamer( + self.hass, backup, open_stream, password + ) + if streamer: + open_stream_func = streamer.open_stream + _backup = replace( + backup, protected=should_encrypt, size=streamer.size() + ) + await self.backup_agents[agent_id].async_upload_backup( + open_stream=open_stream_func, + backup=_backup, + ) + if streamer: + await streamer.wait() + + sync_backup_results = await asyncio.gather( + *(upload_backup_to_agent(agent_id) for agent_id in agent_ids), return_exceptions=True, ) for idx, result in enumerate(sync_backup_results): @@ -506,7 +557,7 @@ class BackupManager: agent_backup, await instance_id.async_get(self.hass) ) backups[backup_id] = ManagerBackup( - agent_ids=[], + agents={}, addons=agent_backup.addons, backup_id=backup_id, date=agent_backup.date, @@ -517,11 +568,12 @@ class BackupManager: homeassistant_included=agent_backup.homeassistant_included, homeassistant_version=agent_backup.homeassistant_version, name=agent_backup.name, - protected=agent_backup.protected, - size=agent_backup.size, with_automatic_settings=with_automatic_settings, ) - backups[backup_id].agent_ids.append(agent_ids[idx]) + backups[backup_id].agents[agent_ids[idx]] = AgentBackupStatus( + protected=agent_backup.protected, + size=agent_backup.size, + ) return (backups, agent_errors) @@ -557,7 +609,7 @@ class BackupManager: result, await instance_id.async_get(self.hass) ) backup = ManagerBackup( - agent_ids=[], + agents={}, addons=result.addons, backup_id=result.backup_id, date=result.date, @@ -568,11 +620,12 @@ class BackupManager: homeassistant_included=result.homeassistant_included, homeassistant_version=result.homeassistant_version, name=result.name, - protected=result.protected, - size=result.size, with_automatic_settings=with_automatic_settings, ) - backup.agent_ids.append(agent_ids[idx]) + backup.agents[agent_ids[idx]] = AgentBackupStatus( + protected=result.protected, + size=result.size, + ) return (backup, agent_errors) @@ -688,6 +741,9 @@ class BackupManager: backup=written_backup.backup, agent_ids=agent_ids, open_stream=written_backup.open_stream, + # When receiving a backup, we don't decrypt or encrypt it according to the + # agent settings, we just upload it as is. + password=None, ) await written_backup.release_stream() self.known_backups.add(written_backup.backup, agent_errors) @@ -855,7 +911,7 @@ class BackupManager: 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), + self._async_finish_backup(agent_ids, with_automatic_settings, password), name="backup_manager_finish_backup", ) if not raise_task_error: @@ -872,7 +928,7 @@ class BackupManager: return new_backup async def _async_finish_backup( - self, agent_ids: list[str], with_automatic_settings: bool + self, agent_ids: list[str], with_automatic_settings: bool, password: str | None ) -> None: """Finish a backup.""" if TYPE_CHECKING: @@ -906,6 +962,7 @@ class BackupManager: backup=written_backup.backup, agent_ids=agent_ids, open_stream=written_backup.open_stream, + password=password, ) finally: await written_backup.release_stream() @@ -1269,6 +1326,10 @@ class CoreBackupReaderWriter(BackupReaderWriter): """Generate a backup.""" manager = self._hass.data[DATA_MANAGER] + agent_config = manager.config.data.agents.get(self._local_agent_id) + if agent_config and not agent_config.protected: + password = None + local_agent_tar_file_path = None if self._local_agent_id in agent_ids: local_agent = manager.local_backup_agents[self._local_agent_id] diff --git a/homeassistant/components/backup/models.py b/homeassistant/components/backup/models.py index f2a83f50c17..1543d577964 100644 --- a/homeassistant/components/backup/models.py +++ b/homeassistant/components/backup/models.py @@ -28,7 +28,7 @@ class Folder(StrEnum): @dataclass(frozen=True, kw_only=True) -class AgentBackup: +class BaseBackup: """Base backup class.""" addons: list[AddonInfo] @@ -40,12 +40,6 @@ class AgentBackup: homeassistant_included: bool homeassistant_version: str | None # None if homeassistant_included is False name: str - protected: bool - size: int - - def as_dict(self) -> dict: - """Return a dict representation of this backup.""" - return asdict(self) def as_frontend_json(self) -> dict: """Return a dict representation of this backup for sending to frontend.""" @@ -53,6 +47,18 @@ class AgentBackup: key: val for key, val in asdict(self).items() if key != "extra_metadata" } + +@dataclass(frozen=True, kw_only=True) +class AgentBackup(BaseBackup): + """Agent backup class.""" + + protected: bool + size: int + + def as_dict(self) -> dict: + """Return a dict representation of this backup.""" + return asdict(self) + @classmethod def from_dict(cls, data: dict[str, Any]) -> Self: """Create an instance from a JSON serialization.""" diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index 0e1c49426c5..3e2a88b8168 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -48,7 +48,9 @@ class _BackupStore(Store[StoredBackupData]): data = old_data if old_major_version == 1: if old_minor_version < 2: - # Version 1.2 adds configurable backup time and custom days + # Version 1.2 adds per agent settings, configurable backup time + # and custom days + data["config"]["agents"] = {} data["config"]["schedule"]["time"] = None if (state := data["config"]["schedule"]["state"]) in ("daily", "never"): data["config"]["schedule"]["days"] = [] diff --git a/homeassistant/components/backup/util.py b/homeassistant/components/backup/util.py index e5acf974012..bea3fe1f4ef 100644 --- a/homeassistant/components/backup/util.py +++ b/homeassistant/components/backup/util.py @@ -3,14 +3,17 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncIterator, Callable +from collections.abc import AsyncIterator, Callable, Coroutine import copy +from dataclasses import dataclass, replace from io import BytesIO import json +import os from pathlib import Path, PurePath from queue import SimpleQueue import tarfile -from typing import IO, Self, cast +import threading +from typing import IO, Any, Self, cast import aiohttp from securetar import SecureTarError, SecureTarFile, SecureTarReadError @@ -30,6 +33,12 @@ class DecryptError(HomeAssistantError): _message = "Unexpected error during decryption." +class EncryptError(HomeAssistantError): + """Error during encryption.""" + + _message = "Unexpected error during encryption." + + class UnsupportedSecureTarVersion(DecryptError): """Unsupported securetar version.""" @@ -179,6 +188,7 @@ class AsyncIteratorWriter: def __init__(self, hass: HomeAssistant) -> None: """Initialize the wrapper.""" self._hass = hass + self._pos: int = 0 self._queue: asyncio.Queue[bytes | None] = asyncio.Queue(maxsize=1) def __aiter__(self) -> Self: @@ -191,9 +201,14 @@ class AsyncIteratorWriter: return data raise StopAsyncIteration + def tell(self) -> int: + """Return the current position in the iterator.""" + return self._pos + def write(self, s: bytes, /) -> int: """Write data to the iterator.""" asyncio.run_coroutine_threadsafe(self._queue.put(s), self._hass.loop).result() + self._pos += len(s) return len(s) @@ -230,9 +245,12 @@ def decrypt_backup( input_stream: IO[bytes], output_stream: IO[bytes], password: str | None, - on_done: Callable[[], None], + on_done: Callable[[Exception | None], None], + minimum_size: int, + nonces: list[bytes], ) -> None: """Decrypt a backup.""" + error: Exception | None = None try: with ( tarfile.open( @@ -245,9 +263,14 @@ def decrypt_backup( _decrypt_backup(input_tar, output_tar, password) except (DecryptError, SecureTarError, tarfile.TarError) as err: LOGGER.warning("Error decrypting backup: %s", err) + error = err + else: + # Pad the output stream to the requested minimum size + padding = max(minimum_size - output_stream.tell(), 0) + output_stream.write(b"\0" * padding) finally: output_stream.write(b"") # Write an empty chunk to signal the end of the stream - on_done() + on_done(error) def _decrypt_backup( @@ -288,6 +311,180 @@ def _decrypt_backup( output_tar.addfile(decrypted_obj, decrypted) +def encrypt_backup( + input_stream: IO[bytes], + output_stream: IO[bytes], + password: str | None, + on_done: Callable[[Exception | None], None], + minimum_size: int, + nonces: list[bytes], +) -> None: + """Encrypt a backup.""" + error: Exception | None = None + try: + with ( + tarfile.open( + fileobj=input_stream, mode="r|", bufsize=BUF_SIZE + ) as input_tar, + tarfile.open( + fileobj=output_stream, mode="w|", bufsize=BUF_SIZE + ) as output_tar, + ): + _encrypt_backup(input_tar, output_tar, password, nonces) + except (EncryptError, SecureTarError, tarfile.TarError) as err: + LOGGER.warning("Error encrypting backup: %s", err) + error = err + else: + # Pad the output stream to the requested minimum size + padding = max(minimum_size - output_stream.tell(), 0) + output_stream.write(b"\0" * padding) + finally: + output_stream.write(b"") # Write an empty chunk to signal the end of the stream + on_done(error) + + +def _encrypt_backup( + input_tar: tarfile.TarFile, + output_tar: tarfile.TarFile, + password: str | None, + nonces: list[bytes], +) -> None: + """Encrypt a backup.""" + inner_tar_idx = 0 + for obj in input_tar: + # We compare with PurePath to avoid issues with different path separators, + # for example when backup.json is added as "./backup.json" + if PurePath(obj.name) == PurePath("backup.json"): + # Rewrite the backup.json file to indicate that the backup is encrypted + if not (reader := input_tar.extractfile(obj)): + raise EncryptError + metadata = json_loads_object(reader.read()) + metadata["protected"] = True + updated_metadata_b = json.dumps(metadata).encode() + metadata_obj = copy.deepcopy(obj) + metadata_obj.size = len(updated_metadata_b) + output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b)) + continue + if not obj.name.endswith((".tar", ".tgz", ".tar.gz")): + output_tar.addfile(obj, input_tar.extractfile(obj)) + continue + istf = SecureTarFile( + None, # Not used + gzip=False, + key=password_to_key(password) if password is not None else None, + mode="r", + fileobj=input_tar.extractfile(obj), + nonce=nonces[inner_tar_idx], + ) + inner_tar_idx += 1 + with istf.encrypt(obj) as encrypted: + encrypted_obj = copy.deepcopy(obj) + encrypted_obj.size = encrypted.encrypted_size + output_tar.addfile(encrypted_obj, encrypted) + + +@dataclass(kw_only=True) +class _CipherWorkerStatus: + done: asyncio.Event + error: Exception | None = None + thread: threading.Thread + + +class _CipherBackupStreamer: + """Encrypt or decrypt a backup.""" + + _cipher_func: Callable[ + [ + IO[bytes], + IO[bytes], + str | None, + Callable[[Exception | None], None], + int, + list[bytes], + ], + None, + ] + + def __init__( + self, + hass: HomeAssistant, + backup: AgentBackup, + open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]], + password: str | None, + ) -> None: + """Initialize.""" + self._workers: list[_CipherWorkerStatus] = [] + self._backup = backup + self._hass = hass + self._open_stream = open_stream + self._password = password + self._nonces: list[bytes] = [] + + def size(self) -> int: + """Return the maximum size of the decrypted or encrypted backup.""" + return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE + + def _num_tar_files(self) -> int: + """Return the number of inner tar files.""" + b = self._backup + return len(b.addons) + len(b.folders) + b.homeassistant_included + 1 + + async def open_stream(self) -> AsyncIterator[bytes]: + """Open a stream.""" + + def on_done(error: Exception | None) -> None: + """Call by the worker thread when it's done.""" + worker_status.error = error + self._hass.loop.call_soon_threadsafe(worker_status.done.set) + + stream = await self._open_stream() + reader = AsyncIteratorReader(self._hass, stream) + writer = AsyncIteratorWriter(self._hass) + worker = threading.Thread( + target=self._cipher_func, + args=[reader, writer, self._password, on_done, self.size(), self._nonces], + ) + worker_status = _CipherWorkerStatus(done=asyncio.Event(), thread=worker) + self._workers.append(worker_status) + worker.start() + return writer + + async def wait(self) -> None: + """Wait for the worker threads to finish.""" + await asyncio.gather(*(worker.done.wait() for worker in self._workers)) + + +class DecryptedBackupStreamer(_CipherBackupStreamer): + """Decrypt a backup.""" + + _cipher_func = staticmethod(decrypt_backup) + + def backup(self) -> AgentBackup: + """Return the decrypted backup.""" + return replace(self._backup, protected=False, size=self.size()) + + +class EncryptedBackupStreamer(_CipherBackupStreamer): + """Encrypt a backup.""" + + def __init__( + self, + hass: HomeAssistant, + backup: AgentBackup, + open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]], + password: str | None, + ) -> None: + """Initialize.""" + super().__init__(hass, backup, open_stream, password) + self._nonces = [os.urandom(16) for _ in range(self._num_tar_files())] + + _cipher_func = staticmethod(encrypt_backup) + + def backup(self) -> AgentBackup: + """Return the encrypted backup.""" + return replace(self._backup, protected=True, size=self.size()) + + async def receive_file( hass: HomeAssistant, contents: aiohttp.BodyPartReader, path: Path ) -> None: diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 74f56102670..d8a425ab6ba 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -198,7 +198,7 @@ async def handle_can_decrypt_on_download( vol.Optional("include_folders"): [vol.Coerce(Folder)], vol.Optional("include_homeassistant", default=True): bool, vol.Optional("name"): str, - vol.Optional("password"): str, + vol.Optional("password"): vol.Any(str, None), } ) @websocket_api.async_response @@ -344,6 +344,7 @@ async def handle_config_info( @websocket_api.websocket_command( { vol.Required("type"): "backup/config/update", + vol.Optional("agents"): vol.Schema({str: {"protected": bool}}), vol.Optional("create_backup"): vol.Schema( { vol.Optional("agent_ids"): vol.All([str], vol.Unique()), diff --git a/tests/components/backup/conftest.py b/tests/components/backup/conftest.py index 7831efeff9a..bef48498ede 100644 --- a/tests/components/backup/conftest.py +++ b/tests/components/backup/conftest.py @@ -72,6 +72,7 @@ def mock_create_backup() -> Generator[AsyncMock]: """Mock manager create backup.""" mock_written_backup = MagicMock(spec_set=WrittenBackup) mock_written_backup.backup.backup_id = "abc123" + mock_written_backup.backup.protected = False mock_written_backup.open_stream = AsyncMock() mock_written_backup.release_stream = AsyncMock() fut: Future[MagicMock] = Future() diff --git a/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted new file mode 100644 index 0000000000000000000000000000000000000000..c97533fc1afb35fafb7be57651fc72e4069f44b3 GIT binary patch literal 10240 zcmdPXPfASAE-lc@D$dVipbanp0y7g61`rJd=(K^Uu^~(hB5!D5WMasmU_cw^pqg4* zT#{G>v>sJ-#PF(>lJj#5ic*VRNu4klY zpqG+bW}pNzC@(P=>?vnpl;`IvK+?Sesyd*uf};GA)Z`LyaDW{F6f4dtO$Qm8Y>=E} zYMhh;a(YQ+0ob^L#G;bS#2k+&OtSGgy(-F3x(X0%-mF4Lvv$u149cV3u8SC14{#Ab0aR)fEwiu z#}G))FG|$|)_{8HRW$P+C{yFB|IJK{7z|C!O^ggpOwCMzWq~0Gj@JJ)ix4D(<-0jJ zR-f)jXZp|ZcElJxxFZT|7>;M;dW=HA|yIR4BHo>;un?)+hk0>-8bvYCy!p}Q~n z`|s-X$OwGyTE!bS>j=kEp2=1pd6b%BY;^MK`X*cndo6GiYHg=zif>8FPOb#7{4Iw5|p4L4d_^LiZEiSAmx*7&MTgiYCm_`hR4%|FPEp#uk=l<`$#%KTUGn;4&0c z{~OV`0YFauZ)QAN|I;I-jMo37_5Z;4|BVr20ibaL;P{^*u>C*U|EGNkU}TP^|8HVw zWNbWI{nI}52i^uy{ck)N>wlBc`kx-DW3>Js+4Vm?(%7gSqaiRF0;3@?8UmvsFd71* QAut*OqaiRF0)rz203J%KF#rGn literal 0 HcmV?d00001 diff --git a/tests/components/backup/snapshots/test_backup.ambr b/tests/components/backup/snapshots/test_backup.ambr index 1a6774e7a95..441f79276a5 100644 --- a/tests/components/backup/snapshots/test_backup.ambr +++ b/tests/components/backup/snapshots/test_backup.ambr @@ -62,9 +62,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -77,8 +80,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index 45af91645ad..7069860638a 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -11,6 +11,8 @@ }), ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -53,6 +55,8 @@ }), ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -96,6 +100,11 @@ }), ]), 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + }), + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -138,6 +147,11 @@ }), ]), 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + }), + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 634404b09cd..f5a22201138 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -234,6 +234,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -269,6 +271,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -316,6 +320,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -352,6 +358,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -388,6 +396,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -425,6 +435,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -461,6 +473,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -494,11 +508,59 @@ 'type': 'result', }) # --- -# name: test_config_update[command0] +# name: test_config_info[storage_data7] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + 'test-agent1': dict({ + 'protected': True, + }), + 'test-agent2': dict({ + 'protected': False, + }), + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': '2024-11-17T04:55:00+01:00', + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + 'mon', + 'sun', + ]), + 'recurrence': 'custom_days', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[commands0] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -529,11 +591,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command0].1 +# name: test_config_update[commands0].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -565,12 +629,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command0].2 +# name: test_config_update[commands0].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -602,11 +668,13 @@ 'version': 1, }) # --- -# name: test_config_update[command10] +# name: test_config_update[commands10] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -637,11 +705,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command10].1 +# name: test_config_update[commands10].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -673,12 +743,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command10].2 +# name: test_config_update[commands10].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -710,11 +782,13 @@ 'version': 1, }) # --- -# name: test_config_update[command11] +# name: test_config_update[commands11] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -745,11 +819,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command11].1 +# name: test_config_update[commands11].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -781,12 +857,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command11].2 +# name: test_config_update[commands11].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -818,11 +896,13 @@ 'version': 1, }) # --- -# name: test_config_update[command1] +# name: test_config_update[commands12] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -853,11 +933,304 @@ 'type': 'result', }) # --- -# name: test_config_update[command1].1 +# name: test_config_update[commands12].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + 'test-agent1': dict({ + 'protected': True, + }), + 'test-agent2': dict({ + 'protected': False, + }), + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[commands12].2 + dict({ + 'data': dict({ + 'backups': list([ + ]), + 'config': dict({ + 'agents': dict({ + 'test-agent1': dict({ + 'protected': True, + }), + 'test-agent2': dict({ + 'protected': False, + }), + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 2, + 'version': 1, + }) +# --- +# name: test_config_update[commands13] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[commands13].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + 'test-agent1': dict({ + 'protected': True, + }), + 'test-agent2': dict({ + 'protected': False, + }), + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[commands13].2 + dict({ + 'id': 5, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + 'test-agent1': dict({ + 'protected': False, + }), + 'test-agent2': dict({ + 'protected': True, + }), + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[commands13].3 + dict({ + 'data': dict({ + 'backups': list([ + ]), + 'config': dict({ + 'agents': dict({ + 'test-agent1': dict({ + 'protected': False, + }), + 'test-agent2': dict({ + 'protected': True, + }), + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 2, + 'version': 1, + }) +# --- +# name: test_config_update[commands1] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update[commands1].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -889,12 +1262,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command1].2 +# name: test_config_update[commands1].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -926,11 +1301,13 @@ 'version': 1, }) # --- -# name: test_config_update[command2] +# name: test_config_update[commands2] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -961,11 +1338,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command2].1 +# name: test_config_update[commands2].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -998,12 +1377,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command2].2 +# name: test_config_update[commands2].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1036,11 +1417,13 @@ 'version': 1, }) # --- -# name: test_config_update[command3] +# name: test_config_update[commands3] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1071,11 +1454,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command3].1 +# name: test_config_update[commands3].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1107,12 +1492,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command3].2 +# name: test_config_update[commands3].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1144,11 +1531,13 @@ 'version': 1, }) # --- -# name: test_config_update[command4] +# name: test_config_update[commands4] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1179,11 +1568,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command4].1 +# name: test_config_update[commands4].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1217,12 +1608,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command4].2 +# name: test_config_update[commands4].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1256,11 +1649,13 @@ 'version': 1, }) # --- -# name: test_config_update[command5] +# name: test_config_update[commands5] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1291,11 +1686,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command5].1 +# name: test_config_update[commands5].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1331,12 +1728,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command5].2 +# name: test_config_update[commands5].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1372,11 +1771,13 @@ 'version': 1, }) # --- -# name: test_config_update[command6] +# name: test_config_update[commands6] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1407,11 +1808,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command6].1 +# name: test_config_update[commands6].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1443,12 +1846,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command6].2 +# name: test_config_update[commands6].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1480,11 +1885,13 @@ 'version': 1, }) # --- -# name: test_config_update[command7] +# name: test_config_update[commands7] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1515,11 +1922,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command7].1 +# name: test_config_update[commands7].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1551,12 +1960,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command7].2 +# name: test_config_update[commands7].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1588,11 +1999,13 @@ 'version': 1, }) # --- -# name: test_config_update[command8] +# name: test_config_update[commands8] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1623,11 +2036,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command8].1 +# name: test_config_update[commands8].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1659,12 +2074,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command8].2 +# name: test_config_update[commands8].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1696,11 +2113,13 @@ 'version': 1, }) # --- -# name: test_config_update[command9] +# name: test_config_update[commands9] dict({ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1731,11 +2150,13 @@ 'type': 'result', }) # --- -# name: test_config_update[command9].1 +# name: test_config_update[commands9].1 dict({ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1767,12 +2188,14 @@ 'type': 'result', }) # --- -# name: test_config_update[command9].2 +# name: test_config_update[commands9].2 dict({ 'data': dict({ 'backups': list([ ]), 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ 'test-agent', @@ -1809,6 +2232,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1844,6 +2269,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1879,6 +2306,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1914,6 +2343,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1949,6 +2380,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -1984,6 +2417,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2019,6 +2454,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2054,6 +2491,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2089,6 +2528,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2124,6 +2565,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2159,6 +2602,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2194,6 +2639,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2229,6 +2676,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2264,6 +2713,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2299,6 +2750,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2334,6 +2787,8 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2369,6 +2824,8 @@ 'id': 1, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2404,6 +2861,82 @@ 'id': 3, 'result': dict({ 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command9] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command9].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), 'create_backup': dict({ 'agent_ids': list([ ]), @@ -2494,9 +3027,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -2509,8 +3045,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -2566,9 +3100,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'test.remote', - ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -2581,8 +3118,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -2621,9 +3156,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'test.remote', - ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -2636,8 +3174,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -2660,9 +3196,12 @@ dict({ 'addons': list([ ]), - 'agent_ids': list([ - 'test.remote', - ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': False, + 'size': 1, + }), + }), 'backup_id': 'def456', 'database_included': False, 'date': '1980-01-01T00:00:00.000Z', @@ -2675,8 +3214,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test 2', - 'protected': False, - 'size': 1, 'with_automatic_settings': None, }), ]), @@ -2710,9 +3247,12 @@ dict({ 'addons': list([ ]), - 'agent_ids': list([ - 'test.remote', - ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': False, + 'size': 1, + }), + }), 'backup_id': 'def456', 'database_included': False, 'date': '1980-01-01T00:00:00.000Z', @@ -2725,8 +3265,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test 2', - 'protected': False, - 'size': 1, 'with_automatic_settings': None, }), ]), @@ -2754,10 +3292,16 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'test.remote', - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + 'test.remote': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -2770,8 +3314,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -2810,9 +3352,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'test.remote', - ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -2825,8 +3370,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -2866,9 +3409,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'domain.test', - ]), + 'agents': dict({ + 'domain.test': dict({ + 'protected': False, + 'size': 13, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00Z', @@ -2881,8 +3427,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 13, 'with_automatic_settings': None, }), ]), @@ -2922,9 +3466,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'domain.test', - ]), + 'agents': dict({ + 'domain.test': dict({ + 'protected': False, + 'size': 13, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00Z', @@ -2938,8 +3485,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 13, 'with_automatic_settings': None, }), ]), @@ -2978,9 +3523,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'domain.test', - ]), + 'agents': dict({ + 'domain.test': dict({ + 'protected': False, + 'size': 13, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00Z', @@ -2993,8 +3541,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 13, 'with_automatic_settings': None, }), ]), @@ -3033,9 +3579,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'domain.test', - ]), + 'agents': dict({ + 'domain.test': dict({ + 'protected': False, + 'size': 13, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00Z', @@ -3048,8 +3597,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 13, 'with_automatic_settings': None, }), ]), @@ -3088,9 +3635,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'domain.test', - ]), + 'agents': dict({ + 'domain.test': dict({ + 'protected': False, + 'size': 13, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00Z', @@ -3103,8 +3653,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 13, 'with_automatic_settings': None, }), ]), @@ -3143,9 +3691,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'domain.test', - ]), + 'agents': dict({ + 'domain.test': dict({ + 'protected': False, + 'size': 13, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00Z', @@ -3159,8 +3710,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 13, 'with_automatic_settings': None, }), ]), @@ -3199,9 +3748,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3214,8 +3766,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), }), @@ -3237,9 +3787,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'test.remote', - ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3252,8 +3805,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), }), @@ -3287,10 +3838,16 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'test.remote', - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + 'test.remote': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3303,8 +3860,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), }), @@ -3327,9 +3882,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3342,8 +3900,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), }), @@ -3602,9 +4158,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3617,8 +4176,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -3646,9 +4203,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3661,8 +4221,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -3690,10 +4248,16 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'test.remote', - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + 'test.remote': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3706,8 +4270,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -3730,9 +4292,12 @@ dict({ 'addons': list([ ]), - 'agent_ids': list([ - 'test.remote', - ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': False, + 'size': 1, + }), + }), 'backup_id': 'def456', 'database_included': False, 'date': '1980-01-01T00:00:00.000Z', @@ -3745,8 +4310,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test 2', - 'protected': False, - 'size': 1, 'with_automatic_settings': None, }), dict({ @@ -3757,9 +4320,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3772,8 +4338,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), @@ -3802,9 +4366,12 @@ 'version': '1.0.0', }), ]), - 'agent_ids': list([ - 'backup.local', - ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': False, + 'size': 0, + }), + }), 'backup_id': 'abc123', 'database_included': True, 'date': '1970-01-01T00:00:00.000Z', @@ -3817,8 +4384,6 @@ 'homeassistant_included': True, 'homeassistant_version': '2024.12.0', 'name': 'Test', - 'protected': False, - 'size': 0, 'with_automatic_settings': True, }), ]), diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index f2c2e5c5b05..d2993e53410 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -487,11 +487,13 @@ async def test_initiate_backup( result = await ws_client.receive_json() backup_data = result["result"]["backup"] - backup_agent_ids = backup_data.pop("agent_ids") - assert backup_agent_ids == agent_ids assert backup_data == { "addons": [], + "agents": { + agent_id: {"protected": bool(password), "size": ANY} + for agent_id in agent_ids + }, "backup_id": backup_id, "database_included": include_database, "date": ANY, @@ -500,8 +502,6 @@ async def test_initiate_backup( "homeassistant_included": True, "homeassistant_version": "2025.1.0", "name": name, - "protected": bool(password), - "size": ANY, "with_automatic_settings": False, } @@ -543,9 +543,7 @@ async def test_initiate_backup_with_agent_error( "version": "1.0.0", }, ], - "agent_ids": [ - "test.remote", - ], + "agents": {"test.remote": {"protected": False, "size": 0}}, "backup_id": "backup1", "database_included": True, "date": "1970-01-01T00:00:00.000Z", @@ -557,15 +555,11 @@ async def test_initiate_backup_with_agent_error( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 0, "with_automatic_settings": True, }, { "addons": [], - "agent_ids": [ - "test.remote", - ], + "agents": {"test.remote": {"protected": False, "size": 1}}, "backup_id": "backup2", "database_included": False, "date": "1980-01-01T00:00:00.000Z", @@ -577,8 +571,6 @@ async def test_initiate_backup_with_agent_error( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test 2", - "protected": False, - "size": 1, "with_automatic_settings": None, }, { @@ -589,9 +581,7 @@ async def test_initiate_backup_with_agent_error( "version": "1.0.0", }, ], - "agent_ids": [ - "test.remote", - ], + "agents": {"test.remote": {"protected": False, "size": 0}}, "backup_id": "backup3", "database_included": True, "date": "1970-01-01T00:00:00.000Z", @@ -603,8 +593,6 @@ async def test_initiate_backup_with_agent_error( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 0, "with_automatic_settings": True, }, ] @@ -714,7 +702,7 @@ async def test_initiate_backup_with_agent_error( new_expected_backup_data = { "addons": [], - "agent_ids": ["backup.local"], + "agents": {"backup.local": {"protected": False, "size": 123}}, "backup_id": "abc123", "database_included": True, "date": ANY, @@ -723,8 +711,6 @@ async def test_initiate_backup_with_agent_error( "homeassistant_included": True, "homeassistant_version": "2025.1.0", "name": "Custom backup 2025.1.0", - "protected": False, - "size": 123, "with_automatic_settings": False, } @@ -1633,9 +1619,7 @@ async def test_receive_backup_agent_error( "version": "1.0.0", }, ], - "agent_ids": [ - "test.remote", - ], + "agents": {"test.remote": {"protected": False, "size": 0}}, "backup_id": "backup1", "database_included": True, "date": "1970-01-01T00:00:00.000Z", @@ -1647,15 +1631,11 @@ async def test_receive_backup_agent_error( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 0, "with_automatic_settings": True, }, { "addons": [], - "agent_ids": [ - "test.remote", - ], + "agents": {"test.remote": {"protected": False, "size": 1}}, "backup_id": "backup2", "database_included": False, "date": "1980-01-01T00:00:00.000Z", @@ -1667,8 +1647,6 @@ async def test_receive_backup_agent_error( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test 2", - "protected": False, - "size": 1, "with_automatic_settings": None, }, { @@ -1679,9 +1657,7 @@ async def test_receive_backup_agent_error( "version": "1.0.0", }, ], - "agent_ids": [ - "test.remote", - ], + "agents": {"test.remote": {"protected": False, "size": 0}}, "backup_id": "backup3", "database_included": True, "date": "1970-01-01T00:00:00.000Z", @@ -1693,8 +1669,6 @@ async def test_receive_backup_agent_error( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 0, "with_automatic_settings": True, }, ] @@ -2936,3 +2910,220 @@ async def test_restore_backup_file_error( assert open_mock.return_value.close.call_count == close_call_count assert mocked_write_text.call_count == write_text_call_count assert mocked_service_call.call_count == 0 + + +@pytest.mark.parametrize( + ("commands", "password", "protected_backup"), + [ + ( + [], + None, + {"backup.local": False, "test.remote": False}, + ), + ( + [], + "hunter2", + {"backup.local": True, "test.remote": True}, + ), + ( + [ + { + "type": "backup/config/update", + "agents": { + "backup.local": {"protected": False}, + "test.remote": {"protected": False}, + }, + } + ], + "hunter2", + {"backup.local": False, "test.remote": False}, + ), + ( + [ + { + "type": "backup/config/update", + "agents": { + "backup.local": {"protected": False}, + "test.remote": {"protected": True}, + }, + } + ], + "hunter2", + {"backup.local": False, "test.remote": True}, + ), + ( + [ + { + "type": "backup/config/update", + "agents": { + "backup.local": {"protected": True}, + "test.remote": {"protected": False}, + }, + } + ], + "hunter2", + {"backup.local": True, "test.remote": False}, + ), + ( + [ + { + "type": "backup/config/update", + "agents": { + "backup.local": {"protected": True}, + "test.remote": {"protected": True}, + }, + } + ], + "hunter2", + {"backup.local": True, "test.remote": True}, + ), + ( + [ + { + "type": "backup/config/update", + "agents": { + "backup.local": {"protected": False}, + "test.remote": {"protected": True}, + }, + } + ], + None, + {"backup.local": False, "test.remote": False}, + ), + ], +) +@pytest.mark.usefixtures("mock_backup_generation") +async def test_initiate_backup_per_agent_encryption( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + generate_backup_id: MagicMock, + path_glob: MagicMock, + commands: dict[str, Any], + password: str | None, + protected_backup: dict[str, bool], +) -> None: + """Test generate backup where encryption is selectively set on agents.""" + agent_ids = ["backup.local", "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, + "next_automatic_backup": None, + "next_automatic_backup_additional": False, + } + + for command in commands: + await ws_client.send_json_auto_id(command) + result = await ws_client.receive_json() + assert result["success"] is True + + 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")), + ): + await ws_client.send_json_auto_id( + { + "type": "backup/generate", + "agent_ids": agent_ids, + "password": password, + "name": "test", + } + ) + result = await ws_client.receive_json() + assert result["event"] == { + "manager_state": BackupManagerState.CREATE_BACKUP, + "reason": None, + "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, + "reason": None, + "stage": CreateBackupStage.HOME_ASSISTANT, + "state": CreateBackupState.IN_PROGRESS, + } + + result = await ws_client.receive_json() + assert result["event"] == { + "manager_state": BackupManagerState.CREATE_BACKUP, + "reason": None, + "stage": CreateBackupStage.UPLOAD_TO_AGENTS, + "state": CreateBackupState.IN_PROGRESS, + } + + result = await ws_client.receive_json() + assert result["event"] == { + "manager_state": BackupManagerState.CREATE_BACKUP, + "reason": None, + "stage": None, + "state": CreateBackupState.COMPLETED, + } + + result = await ws_client.receive_json() + assert result["event"] == {"manager_state": BackupManagerState.IDLE} + + await ws_client.send_json_auto_id( + {"type": "backup/details", "backup_id": backup_id} + ) + result = await ws_client.receive_json() + + backup_data = result["result"]["backup"] + + assert backup_data == { + "addons": [], + "agents": { + agent_id: {"protected": protected_backup[agent_id], "size": ANY} + for agent_id in agent_ids + }, + "backup_id": backup_id, + "database_included": True, + "date": ANY, + "failed_agent_ids": [], + "folders": [], + "homeassistant_included": True, + "homeassistant_version": "2025.1.0", + "name": "test", + "with_automatic_settings": False, + } diff --git a/tests/components/backup/test_store.py b/tests/components/backup/test_store.py index cc84b66340c..f05afbea9ec 100644 --- a/tests/components/backup/test_store.py +++ b/tests/components/backup/test_store.py @@ -66,6 +66,7 @@ def mock_delay_save() -> Generator[None]: } ], "config": { + "agents": {"test.remote": {"protected": True}}, "create_backup": { "agent_ids": [], "include_addons": None, diff --git a/tests/components/backup/test_util.py b/tests/components/backup/test_util.py index 60cfc77b1aa..db759805c8f 100644 --- a/tests/components/backup/test_util.py +++ b/tests/components/backup/test_util.py @@ -2,13 +2,24 @@ from __future__ import annotations +from collections.abc import AsyncIterator +import dataclasses import tarfile from unittest.mock import Mock, patch import pytest +import securetar -from homeassistant.components.backup import AddonInfo, AgentBackup, Folder -from homeassistant.components.backup.util import read_backup, validate_password +from homeassistant.components.backup import DOMAIN, AddonInfo, AgentBackup, Folder +from homeassistant.components.backup.util import ( + DecryptedBackupStreamer, + EncryptedBackupStreamer, + read_backup, + validate_password, +) +from homeassistant.core import HomeAssistant + +from tests.common import get_fixture_path @pytest.mark.parametrize( @@ -130,3 +141,246 @@ def test_validate_password_no_homeassistant() -> None: KeyError ) assert validate_password(mock_path, "hunter2") is False + + +async def test_decrypted_backup_streamer(hass: HomeAssistant) -> None: + """Test the decrypted backup streamer.""" + decrypted_backup_path = get_fixture_path( + "test_backups/c0cb53bd.tar.decrypted", DOMAIN + ) + encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) + backup = AgentBackup( + addons=["addon_1", "addon_2"], + backup_id="1234", + date="2024-12-02T07:23:58.261875-05:00", + database_included=False, + extra_metadata={}, + folders=[], + homeassistant_included=True, + homeassistant_version="2024.12.0.dev0", + name="test", + protected=True, + size=encrypted_backup_path.stat().st_size, + ) + expected_padding = b"\0" * 40960 # 4 x 10240 byte of padding + + async def send_backup() -> AsyncIterator[bytes]: + f = encrypted_backup_path.open("rb") + while chunk := f.read(1024): + yield chunk + + async def open_backup() -> AsyncIterator[bytes]: + return send_backup() + + decryptor = DecryptedBackupStreamer(hass, backup, open_backup, "hunter2") + assert decryptor.backup() == dataclasses.replace( + backup, protected=False, size=backup.size + len(expected_padding) + ) + decrypted_stream = await decryptor.open_stream() + decrypted_output = b"" + async for chunk in decrypted_stream: + decrypted_output += chunk + await decryptor.wait() + + # Expect the output to match the stored decrypted backup file, with additional + # padding. + decrypted_backup_data = decrypted_backup_path.read_bytes() + assert decrypted_output == decrypted_backup_data + expected_padding + + +async def test_decrypted_backup_streamer_wrong_password(hass: HomeAssistant) -> None: + """Test the decrypted backup streamer with wrong password.""" + encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) + backup = AgentBackup( + addons=["addon_1", "addon_2"], + backup_id="1234", + date="2024-12-02T07:23:58.261875-05:00", + database_included=False, + extra_metadata={}, + folders=[], + homeassistant_included=True, + homeassistant_version="2024.12.0.dev0", + name="test", + protected=True, + size=encrypted_backup_path.stat().st_size, + ) + + async def send_backup() -> AsyncIterator[bytes]: + f = encrypted_backup_path.open("rb") + while chunk := f.read(1024): + yield chunk + + async def open_backup() -> AsyncIterator[bytes]: + return send_backup() + + decryptor = DecryptedBackupStreamer(hass, backup, open_backup, "wrong_password") + decrypted_stream = await decryptor.open_stream() + async for _ in decrypted_stream: + pass + + await decryptor.wait() + assert isinstance(decryptor._workers[0].error, securetar.SecureTarReadError) + + +async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None: + """Test the encrypted backup streamer.""" + decrypted_backup_path = get_fixture_path( + "test_backups/c0cb53bd.tar.decrypted", DOMAIN + ) + encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) + backup = AgentBackup( + addons=["addon_1", "addon_2"], + backup_id="1234", + date="2024-12-02T07:23:58.261875-05:00", + database_included=False, + extra_metadata={}, + folders=[], + homeassistant_included=True, + homeassistant_version="2024.12.0.dev0", + name="test", + protected=False, + size=decrypted_backup_path.stat().st_size, + ) + expected_padding = b"\0" * 40960 # 4 x 10240 byte of padding + + async def send_backup() -> AsyncIterator[bytes]: + f = decrypted_backup_path.open("rb") + while chunk := f.read(1024): + yield chunk + + async def open_backup() -> AsyncIterator[bytes]: + return send_backup() + + # Patch os.urandom to return values matching the nonce used in the encrypted + # test backup. The backup has three inner tar files, but we need an extra nonce + # for a future planned supervisor.tar. + with patch("os.urandom") as mock_randbytes: + mock_randbytes.side_effect = ( + bytes.fromhex("bd34ea6fc93b0614ce7af2b44b4f3957"), + bytes.fromhex("1296d6f7554e2cb629a3dc4082bae36c"), + bytes.fromhex("8b7a58e48faf2efb23845eb3164382e0"), + bytes.fromhex("00000000000000000000000000000000"), + ) + encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2") + assert encryptor.backup() == dataclasses.replace( + backup, protected=True, size=backup.size + len(expected_padding) + ) + + encrypted_stream = await encryptor.open_stream() + encrypted_output = b"" + async for chunk in encrypted_stream: + encrypted_output += chunk + await encryptor.wait() + + # Expect the output to match the stored encrypted backup file, with additional + # padding. + encrypted_backup_data = encrypted_backup_path.read_bytes() + assert encrypted_output == encrypted_backup_data + expected_padding + + +async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> None: + """Test the encrypted backup streamer.""" + decrypted_backup_path = get_fixture_path( + "test_backups/c0cb53bd.tar.decrypted", DOMAIN + ) + encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) + backup = AgentBackup( + addons=["addon_1", "addon_2"], + backup_id="1234", + date="2024-12-02T07:23:58.261875-05:00", + database_included=False, + extra_metadata={}, + folders=[], + homeassistant_included=True, + homeassistant_version="2024.12.0.dev0", + name="test", + protected=False, + size=decrypted_backup_path.stat().st_size, + ) + + async def send_backup() -> AsyncIterator[bytes]: + f = decrypted_backup_path.open("rb") + while chunk := f.read(1024): + yield chunk + + async def open_backup() -> AsyncIterator[bytes]: + return send_backup() + + encryptor1 = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2") + encryptor2 = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2") + + async def read_stream(stream: AsyncIterator[bytes]) -> bytes: + output = b"" + async for chunk in stream: + output += chunk + return output + + # When reading twice from the same streamer, the same nonce is used. + encrypted_output1 = await read_stream(await encryptor1.open_stream()) + encrypted_output2 = await read_stream(await encryptor1.open_stream()) + assert encrypted_output1 == encrypted_output2 + + encrypted_output3 = await read_stream(await encryptor2.open_stream()) + encrypted_output4 = await read_stream(await encryptor2.open_stream()) + assert encrypted_output3 == encrypted_output4 + + # Wait for workers to terminate + await encryptor1.wait() + await encryptor2.wait() + + # Output from the two streames should differ but have the same length. + assert encrypted_output1 != encrypted_output3 + assert len(encrypted_output1) == len(encrypted_output3) + + # Expect the output length to match the stored encrypted backup file, with + # additional padding. + encrypted_backup_data = encrypted_backup_path.read_bytes() + # 4 x 10240 byte of padding + assert len(encrypted_output1) == len(encrypted_backup_data) + 40960 + assert encrypted_output1[: len(encrypted_backup_data)] != encrypted_backup_data + + +async def test_encrypted_backup_streamer_error(hass: HomeAssistant) -> None: + """Test the encrypted backup streamer.""" + decrypted_backup_path = get_fixture_path( + "test_backups/c0cb53bd.tar.decrypted", DOMAIN + ) + backup = AgentBackup( + addons=["addon_1", "addon_2"], + backup_id="1234", + date="2024-12-02T07:23:58.261875-05:00", + database_included=False, + extra_metadata={}, + folders=[], + homeassistant_included=True, + homeassistant_version="2024.12.0.dev0", + name="test", + protected=False, + size=decrypted_backup_path.stat().st_size, + ) + + async def send_backup() -> AsyncIterator[bytes]: + f = decrypted_backup_path.open("rb") + while chunk := f.read(1024): + yield chunk + + async def open_backup() -> AsyncIterator[bytes]: + return send_backup() + + # Patch os.urandom to return values matching the nonce used in the encrypted + # test backup. The backup has three inner tar files, but we need an extra nonce + # for a future planned supervisor.tar. + encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2") + + with patch( + "homeassistant.components.backup.util.tarfile.open", + side_effect=tarfile.TarError, + ): + encrypted_stream = await encryptor.open_stream() + async for _ in encrypted_stream: + pass + + # Expect the output to match the stored encrypted backup file, with additional + # padding. + await encryptor.wait() + assert isinstance(encryptor._workers[0].error, tarfile.TarError) diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 0fd0ba308b3..613c0b69b6b 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -56,6 +56,7 @@ BACKUP_CALL = call( DEFAULT_STORAGE_DATA: dict[str, Any] = { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": [], "include_addons": None, @@ -587,6 +588,8 @@ async def test_generate_with_default_settings_calls_create( last_completed_automatic_backup: str, ) -> None: """Test backup/generate_with_automatic_settings calls async_initiate_backup.""" + created_backup: MagicMock = create_backup.return_value[1].result().backup + created_backup.protected = create_backup_settings["password"] is not None client = await hass_ws_client(hass) await hass.config.async_set_time_zone("Europe/Amsterdam") freezer.move_to("2024-11-13T12:01:00+01:00") @@ -913,6 +916,7 @@ async def test_agents_info( "data": { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": ["test-addon"], @@ -943,6 +947,7 @@ async def test_agents_info( "data": { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": None, @@ -973,6 +978,7 @@ async def test_agents_info( "data": { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": None, @@ -1003,6 +1009,7 @@ async def test_agents_info( "data": { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": None, @@ -1033,6 +1040,7 @@ async def test_agents_info( "data": { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": None, @@ -1063,6 +1071,41 @@ async def test_agents_info( "data": { "backups": [], "config": { + "agents": {}, + "create_backup": { + "agent_ids": ["test-agent"], + "include_addons": None, + "include_all_addons": False, + "include_database": False, + "include_folders": None, + "name": None, + "password": None, + }, + "retention": {"copies": None, "days": None}, + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "schedule": { + "days": ["mon", "sun"], + "recurrence": "custom_days", + "state": "never", + "time": None, + }, + }, + }, + "key": DOMAIN, + "version": store.STORAGE_VERSION, + "minor_version": store.STORAGE_VERSION_MINOR, + }, + }, + { + "backup": { + "data": { + "backups": [], + "config": { + "agents": { + "test-agent1": {"protected": True}, + "test-agent2": {"protected": False}, + }, "create_backup": { "agent_ids": ["test-agent"], "include_addons": None, @@ -1115,80 +1158,130 @@ async def test_config_info( @pytest.mark.usefixtures("create_backup", "delete_backup", "get_backups") @pytest.mark.parametrize( - "command", + "commands", [ - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"copies": None, "days": 7}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"recurrence": "daily", "time": "06:00"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"days": ["mon"], "recurrence": "custom_days"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"recurrence": "never"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "schedule": {"days": ["mon", "sun"], "recurrence": "custom_days"}, - }, - { - "type": "backup/config/update", - "create_backup": { - "agent_ids": ["test-agent"], - "include_addons": ["test-addon"], - "include_folders": ["media"], - "name": "test-name", - "password": "test-password", + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "retention": {"copies": None, "days": 7}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "schedule": {"recurrence": "daily", "time": "06:00"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "schedule": {"days": ["mon"], "recurrence": "custom_days"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "schedule": {"recurrence": "never"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "schedule": {"days": ["mon", "sun"], "recurrence": "custom_days"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": { + "agent_ids": ["test-agent"], + "include_addons": ["test-addon"], + "include_folders": ["media"], + "name": "test-name", + "password": "test-password", + }, + "schedule": {"recurrence": "daily"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "retention": {"copies": 3, "days": 7}, + "schedule": {"recurrence": "daily"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "retention": {"copies": None, "days": None}, + "schedule": {"recurrence": "daily"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "retention": {"copies": 3, "days": None}, + "schedule": {"recurrence": "daily"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "retention": {"copies": None, "days": 7}, + "schedule": {"recurrence": "daily"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "retention": {"copies": 3}, + "schedule": {"recurrence": "daily"}, + } + ], + [ + { + "type": "backup/config/update", + "create_backup": {"agent_ids": ["test-agent"]}, + "retention": {"days": 7}, + "schedule": {"recurrence": "daily"}, + } + ], + [ + { + "type": "backup/config/update", + "agents": { + "test-agent1": {"protected": True}, + "test-agent2": {"protected": False}, + }, + } + ], + [ + # Test we can update AgentConfig + { + "type": "backup/config/update", + "agents": { + "test-agent1": {"protected": True}, + "test-agent2": {"protected": False}, + }, }, - "schedule": {"recurrence": "daily"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"copies": 3, "days": 7}, - "schedule": {"recurrence": "daily"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"copies": None, "days": None}, - "schedule": {"recurrence": "daily"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"copies": 3, "days": None}, - "schedule": {"recurrence": "daily"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"copies": None, "days": 7}, - "schedule": {"recurrence": "daily"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"copies": 3}, - "schedule": {"recurrence": "daily"}, - }, - { - "type": "backup/config/update", - "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"days": 7}, - "schedule": {"recurrence": "daily"}, - }, + { + "type": "backup/config/update", + "agents": { + "test-agent1": {"protected": False}, + "test-agent2": {"protected": True}, + }, + }, + ], ], ) @patch("homeassistant.components.backup.config.random.randint", Mock(return_value=600)) @@ -1197,7 +1290,7 @@ async def test_config_update( hass_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, - command: dict[str, Any], + commands: dict[str, Any], hass_storage: dict[str, Any], ) -> None: """Test updating the backup config.""" @@ -1211,14 +1304,14 @@ async def test_config_update( await client.send_json_auto_id({"type": "backup/config/info"}) assert await client.receive_json() == snapshot - await client.send_json_auto_id(command) - result = await client.receive_json() + for command in commands: + await client.send_json_auto_id(command) + result = await client.receive_json() + assert result["success"] - assert result["success"] - - await client.send_json_auto_id({"type": "backup/config/info"}) - assert await client.receive_json() == snapshot - await hass.async_block_till_done() + await client.send_json_auto_id({"type": "backup/config/info"}) + assert await client.receive_json() == snapshot + await hass.async_block_till_done() # Trigger store write freezer.tick(60) @@ -1274,6 +1367,10 @@ async def test_config_update( "type": "backup/config/update", "create_backup": {"include_folders": ["media", "media"]}, }, + { + "type": "backup/config/update", + "agents": {"test-agent1": {"favorite": True}}, + }, ], ) async def test_config_update_errors( @@ -1600,10 +1697,14 @@ async def test_config_schedule_logic( create_backup_side_effect: list[Exception | None] | None, ) -> None: """Test config schedule logic.""" + created_backup: MagicMock = create_backup.return_value[1].result().backup + created_backup.protected = True + client = await hass_ws_client(hass) storage_data = { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test.test-agent"], "include_addons": ["test-addon"], @@ -2057,10 +2158,14 @@ async def test_config_retention_copies_logic( delete_args_list: Any, ) -> None: """Test config backup retention copies logic.""" + created_backup: MagicMock = create_backup.return_value[1].result().backup + created_backup.protected = True + client = await hass_ws_client(hass) storage_data = { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": ["test-addon"], @@ -2320,10 +2425,14 @@ async def test_config_retention_copies_logic_manual_backup( delete_args_list: Any, ) -> None: """Test config backup retention copies logic for manual backup.""" + created_backup: MagicMock = create_backup.return_value[1].result().backup + created_backup.protected = True + client = await hass_ws_client(hass) storage_data = { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": ["test-addon"], @@ -2750,6 +2859,7 @@ async def test_config_retention_days_logic( storage_data = { "backups": [], "config": { + "agents": {}, "create_backup": { "agent_ids": ["test-agent"], "include_addons": ["test-addon"], diff --git a/tests/components/cloud/test_backup.py b/tests/components/cloud/test_backup.py index 373bd164c0c..516dacd5f3d 100644 --- a/tests/components/cloud/test_backup.py +++ b/tests/components/cloud/test_backup.py @@ -170,6 +170,7 @@ async def test_agents_list_backups( assert response["result"]["backups"] == [ { "addons": [], + "agents": {"cloud.cloud": {"protected": False, "size": 34519040}}, "backup_id": "23e64aec", "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, @@ -177,9 +178,6 @@ async def test_agents_list_backups( "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "protected": False, - "size": 34519040, - "agent_ids": ["cloud.cloud"], "failed_agent_ids": [], "with_automatic_settings": None, } @@ -219,6 +217,7 @@ async def test_agents_list_backups_fail_cloud( "23e64aec", { "addons": [], + "agents": {"cloud.cloud": {"protected": False, "size": 34519040}}, "backup_id": "23e64aec", "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, @@ -226,9 +225,6 @@ async def test_agents_list_backups_fail_cloud( "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "protected": False, - "size": 34519040, - "agent_ids": ["cloud.cloud"], "failed_agent_ids": [], "with_automatic_settings": None, }, diff --git a/tests/components/google_drive/test_backup.py b/tests/components/google_drive/test_backup.py index 765f6bba887..62b7930012c 100644 --- a/tests/components/google_drive/test_backup.py +++ b/tests/components/google_drive/test_backup.py @@ -43,6 +43,7 @@ TEST_AGENT_BACKUP = AgentBackup( ) TEST_AGENT_BACKUP_RESULT = { "addons": [{"name": "Test", "slug": "test", "version": "1.0.0"}], + "agents": {TEST_AGENT_ID: {"protected": False, "size": 987}}, "backup_id": "test-backup", "database_included": True, "date": "2025-01-01T01:23:45.678Z", @@ -50,9 +51,6 @@ TEST_AGENT_BACKUP_RESULT = { "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 987, - "agent_ids": [TEST_AGENT_ID], "failed_agent_ids": [], "with_automatic_settings": None, } diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index 1a5701a79cf..1c257416ad0 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -407,7 +407,7 @@ async def test_agent_info( "addons": [ {"name": "Terminal & SSH", "slug": "core_ssh", "version": "9.14.0"} ], - "agent_ids": ["hassio.local"], + "agents": {"hassio.local": {"protected": False, "size": 1048576}}, "backup_id": "abc123", "database_included": True, "date": "1970-01-01T00:00:00+00:00", @@ -416,8 +416,6 @@ async def test_agent_info( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 1048576, "with_automatic_settings": None, }, ), @@ -428,7 +426,7 @@ async def test_agent_info( "addons": [ {"name": "Terminal & SSH", "slug": "core_ssh", "version": "9.14.0"} ], - "agent_ids": ["hassio.local"], + "agents": {"hassio.local": {"protected": False, "size": 1048576}}, "backup_id": "abc123", "database_included": False, "date": "1970-01-01T00:00:00+00:00", @@ -437,8 +435,6 @@ async def test_agent_info( "homeassistant_included": False, "homeassistant_version": None, "name": "Test", - "protected": False, - "size": 1048576, "with_automatic_settings": None, }, ), diff --git a/tests/components/kitchen_sink/test_backup.py b/tests/components/kitchen_sink/test_backup.py index 827bde39d7d..a664b91393d 100644 --- a/tests/components/kitchen_sink/test_backup.py +++ b/tests/components/kitchen_sink/test_backup.py @@ -102,7 +102,7 @@ async def test_agents_list_backups( assert response["result"]["backups"] == [ { "addons": [{"name": "Test", "slug": "test", "version": "1.0.0"}], - "agent_ids": ["kitchen_sink.syncer"], + "agents": {"kitchen_sink.syncer": {"protected": False, "size": 1234}}, "backup_id": "abc123", "database_included": False, "date": "1970-01-01T00:00:00Z", @@ -111,8 +111,6 @@ async def test_agents_list_backups( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Kitchen sink syncer", - "protected": False, - "size": 1234, "with_automatic_settings": None, } ] @@ -185,7 +183,7 @@ async def test_agents_upload( assert len(backup_list) == 2 assert backup_list[1] == { "addons": [{"name": "Test", "slug": "test", "version": "1.0.0"}], - "agent_ids": ["kitchen_sink.syncer"], + "agents": {"kitchen_sink.syncer": {"protected": False, "size": 0.0}}, "backup_id": "test-backup", "database_included": True, "date": "1970-01-01T00:00:00.000Z", @@ -194,8 +192,6 @@ async def test_agents_upload( "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 0.0, "with_automatic_settings": False, } diff --git a/tests/components/onedrive/test_backup.py b/tests/components/onedrive/test_backup.py index a3cfbe95a46..a3d1129377f 100644 --- a/tests/components/onedrive/test_backup.py +++ b/tests/components/onedrive/test_backup.py @@ -81,6 +81,9 @@ async def test_agents_list_backups( assert response["result"]["backups"] == [ { "addons": [], + "agents": { + "onedrive.mock_drive_id": {"protected": False, "size": 34519040} + }, "backup_id": "23e64aec", "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, @@ -88,9 +91,6 @@ async def test_agents_list_backups( "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "protected": False, - "size": 34519040, - "agent_ids": [f"{DOMAIN}.{mock_config_entry.unique_id}"], "failed_agent_ids": [], "with_automatic_settings": None, } @@ -117,6 +117,12 @@ async def test_agents_get_backup( assert response["result"]["agent_errors"] == {} assert response["result"]["backup"] == { "addons": [], + "agents": { + f"{DOMAIN}.{mock_config_entry.unique_id}": { + "protected": False, + "size": 34519040, + } + }, "backup_id": "23e64aec", "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, @@ -124,9 +130,6 @@ async def test_agents_get_backup( "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "protected": False, - "size": 34519040, - "agent_ids": [f"{DOMAIN}.{mock_config_entry.unique_id}"], "failed_agent_ids": [], "with_automatic_settings": None, } diff --git a/tests/components/synology_dsm/test_backup.py b/tests/components/synology_dsm/test_backup.py index 436e3666176..0d4fd0dc080 100644 --- a/tests/components/synology_dsm/test_backup.py +++ b/tests/components/synology_dsm/test_backup.py @@ -290,6 +290,12 @@ async def test_agents_list_backups( assert response["result"]["backups"] == [ { "addons": [], + "agents": { + "synology_dsm.mocked_syno_dsm_entry": { + "protected": True, + "size": 13916160, + } + }, "backup_id": "abcd12ef", "date": "2025-01-09T20:14:35.457323+01:00", "database_included": True, @@ -297,9 +303,6 @@ async def test_agents_list_backups( "homeassistant_included": True, "homeassistant_version": "2025.2.0.dev0", "name": "Automatic backup 2025.2.0.dev0", - "protected": True, - "size": 13916160, - "agent_ids": ["synology_dsm.mocked_syno_dsm_entry"], "failed_agent_ids": [], "with_automatic_settings": None, } @@ -355,6 +358,12 @@ async def test_agents_list_backups_disabled_filestation( "abcd12ef", { "addons": [], + "agents": { + "synology_dsm.mocked_syno_dsm_entry": { + "protected": True, + "size": 13916160, + } + }, "backup_id": "abcd12ef", "date": "2025-01-09T20:14:35.457323+01:00", "database_included": True, @@ -362,9 +371,6 @@ async def test_agents_list_backups_disabled_filestation( "homeassistant_included": True, "homeassistant_version": "2025.2.0.dev0", "name": "Automatic backup 2025.2.0.dev0", - "protected": True, - "size": 13916160, - "agent_ids": ["synology_dsm.mocked_syno_dsm_entry"], "failed_agent_ids": [], "with_automatic_settings": None, },