core/tests/components/backup/test_websocket.py
Erik Montnemery 8e991fc92f
Merge feature branch with backup changes to dev (#132954)
* Reapply "Make WS command backup/generate send events" (#131530)

This reverts commit 9b8316df3f78d136ae73c096168bd73ffebc4465.

* MVP implementation of Backup sync agents (#126122)

* init sync agent

* add syncing

* root import

* rename list to info and add sync state

* Add base backup class

* Revert unneded change

* adjust tests

* move to kitchen_sink

* split

* move

* Adjustments

* Adjustment

* update

* Tests

* Test unknown agent

* adjust

* Adjust for different test environments

* Change /info WS to contain a dictinary

* reorder

* Add websocket command to trigger sync from the supervisor

* cleanup

* Make mypy happier

---------

Co-authored-by: Erik <erik@montnemery.com>

* Make BackupSyncMetadata model a dataclass (#130555)

Make backup BackupSyncMetadata model a dataclass

* Rename backup sync agent to backup agent (#130575)

* Rename sync agent module to agent

* Rename BackupSyncAgent to BackupAgent

* Fix test typo

* Rename async_get_backup_sync_agents to async_get_backup_agents

* Rename and clean up remaining sync things

* Update kitchen sink

* Apply suggestions from code review

* Update test_manager.py

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Add additional options to WS command backup/generate (#130530)

* Add additional options to WS command backup/generate

* Improve test

* Improve test

* Align parameter names in backup/agents/* WS commands (#130590)

* Allow setting password for backups (#110630)

* Allow setting password for backups

* use is_hassio from helpers

* move it

* Fix getting psw

* Fix restoring with psw

* Address review comments

* Improve docstring

* Adjust kitchen sink

* Adjust

---------

Co-authored-by: Erik <erik@montnemery.com>

* Export relevant names from backup integration (#130596)

* Tweak backup agent interface (#130613)

* Tweak backup agent interface

* Adjust kitchen_sink

* Test kitchen sink backup (#130609)

* Test agents_list_backups

* Test agents_info

* Test agents_download

* Export Backup from manager

* Test agents_upload

* Update tests after rebase

* Use backup domain

* Remove WS command backup/upload (#130588)

* Remove WS command backup/upload

* Disable failing kitchen_sink test

* Make local backup a backup agent (#130623)

* Make local backup a backup agent

* Adjust

* Adjust

* Adjust

* Adjust tests

* Adjust

* Adjust

* Adjust docstring

* Adjust

* Protect members of CoreLocalBackupAgent

* Remove redundant check for file

* Make the backup.create service use the first local agent

* Add BackupAgent.async_get_backup

* Fix some TODOs

* Add support for downloading backup from a remote agent

* Fix restore

* Fix test

* Adjust kitchen_sink test

* Remove unused method BackupManager.async_get_backup_path

* Re-enable kitchen sink test

* Remove BaseBackupManager.async_upload_backup

* Support restore from remote agent

* Fix review comments

* Include backup agent error in response to WS command backup/info (#130884)

* Adjust code related to WS command backup/info (#130890)

* Include backup agent error in response to WS command backup/details (#130892)

* Remove LOCAL_AGENT_ID constant from backup manager (#130895)

* Add backup config storage (#130871)

* Add base for backup config

* Allow updating backup config

* Test loading backup config

* Add backup config update method

* Add temporary check for BackupAgent.async_remove_backup (#130893)

* Rename backup slug to backup_id (#130902)

* Improve backup websocket API tests (#130912)

* Improve backup websocket API tests

* Add missing snapshot

* Fix tests leaving files behind

* Improve backup manager backup creation tests (#130916)

* Remove class backup.backup.LocalBackup (#130919)

* Add agent delete backup (#130921)

* Add backup agent delete backup

* Remove agents delete websocket command

* Update docstring

Co-authored-by: Erik Montnemery <erik@montnemery.com>

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Disable core local backup agent in hassio (#130933)

* Rename remove backup to delete backup (#130940)

* Rename remove backup to delete backup

* Revert "backup/delete"

* Refactor BackupManager (#130947)

* Refactor BackupManager

* Adjust

* Adjust backup creation

* Copy in executor

* Fix BackupManager.async_get_backup (#130975)

* Fix typo in backup tests (#130978)

* Adjust backup NewBackup class (#130976)

* Remove class backup.BackupUploadMetadata (#130977)

Remove class backup.BackupMetadata

* Report backup size in bytes instead of MB (#131028)

Co-authored-by: Robert Resch <robert@resch.dev>

* Speed up CI for feature branch (#131030)

* Speed up CI for feature branch

* adjust

* fix

* fix

* fix

* fix

* Rename remove to delete in backup websocket type (#131023)

* Revert "Speed up CI for feature branch" (#131074)

Revert "Speed up CI for feature branch (#131030)"

This reverts commit 791280506d1859b1a722f5064d75bcbe48acc1c3.

* Rename class BaseBackup to AgentBackup (#131083)

* Rename class BaseBackup to AgentBackup

* Update tests

* Speed up CI for backup feature branch (#131079)

* Add backup platform to the hassio integration (#130991)

* Add backup platform to the hassio integration

* Add hassio to after_dependencies of backup

* Address review comments

* Remove redundant hassio parametrization of tests

* Add tests

* Address review comments

* Bump CI cache version

* Revert "Bump CI cache version"

This reverts commit 2ab4d2b1795c953ccfc9b17c47f9df3faac83749.

* Extend backup info class AgentBackup (#131110)

* Extend backup info class AgentBackup

* Update kitchen sink

* Update kitchen sink test

* Update kitchen sink test

* Exclude cloud and hassio from core files (#131117)

* Remove unnecessary **kwargs from backup API (#131124)

* Fix backup tests (#131128)

* Freeze backup dataclasses (#131122)

* Protect CoreLocalBackupAgent.load_backups (#131126)

* Use backup metadata v2 in core/container backups (#131125)

* Extend backup creation API (#131121)

* Extend backup creation API

* Add tests

* Fix merge

* Fix merge

* Return agent errors when deleting a backup (#131142)

* Return agent errors when deleting a backup

* Remove redundant calls to dict.keys()

* Add enum type for backup folder (#131158)

* Add method AgentBackup.from_dict (#131164)

* Remove WS command backup/agents/list_backups (#131163)

* Handle backup schedule (#131127)

* Add backup schedule handling

* Fix unrelated incorrect type annotation in test

* Clarify delay save

* Make the backup time compatible with the recorder nightly job

* Update create backup parameters

* Use typed dict for create backup parameters

* Simplify schedule state

* Group create backup parameters

* Move parameter

* Fix typo

* Use Folder model

* Handle deserialization of folders better

* Fail on attempt to include addons or folders in core backup (#131204)

* Fix AgentBackup test (#131201)

* Add options to WS command backup/restore (#131194)

* Add options to WS command backup/restore

* Add tests

* Fix test

* Teach core backup to restore only database or only settings (#131225)

* Exclude tmp_backups/*.tar from backups (#131243)

* Add WS command backup/subscribe_events (#131250)

* Clean up temporary directory after restoring backup (#131263)

* Improve hassio backup agent list (#131268)

* Include `last_automatic_backup` in reply to backup/info (#131293)

Include last_automatic_backup in reply to backup/info

* Handle backup delete after config (#131259)

* Handle delete after copies

* Handle delete after days

* Add some test examples

* Test config_delete_after_logic

* Test config_delete_after_copies_logic

* Test more delete after days

* Add debug logs

* Always delete the oldest backup first

* Never remove the last backup

* Clean up words

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Fix after cleaning words

* Use utcnow

* Remove duplicate guard

* Simplify sorting

* Delete backups even if there are agent errors on get backups

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Rename backup delete after to backup retention (#131364)

* Rename backup delete after to backup retention

* Tweak

* Remove length limit on `agent_ids` when configuring backup (#132057)

Remove length limit on agent_ids when configuring backup

* Rename backup retention_config to retention (#132068)

* Modify backup agent API to be stream oriented (#132090)

* Modify backup agent API to be stream oriented

* Fix tests

* Adjust after code review

* Remove no longer needed pylint override

* Improve test coverage

* Change BackupAgent API to work with AsyncIterator objects

* Don't close files in the event loop

* Don't close files in the event loop

* Fix backup manager create backup log (#132174)

* Fix debug log level (#132186)

* Add cloud backup agent (#129621)

* Init cloud backup sync

* Add more metadata

* Fix typo

* Adjust to base changes

* Don't raise on list if more than one backup is available

* Adjust to base branch

* Fetch always and verify on download

* Update homeassistant/components/cloud/backup.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Adjust to base branch changes

* Not required anymore

* Workaround

* Fix blocking event loop

* Fix

* Add some tests

* some tests

* Add cloud backup delete functionality

* Enable check

* Fix ruff

* Use fixture

* Use iter_chunks instead

* Remove read

* Remove explicit export of read_backup

* Align with BackupAgent API changes

* Improve test coverage

* Improve error handling

* Adjust docstrings

* Catch aiohttp.ClientError bubbling up from hass_nabucasa

* Improve iteration

---------

Co-authored-by: Erik <erik@montnemery.com>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Krisjanis Lejejs <krisjanis.lejejs@gmail.com>

* Extract file receiver from `BackupManager.async_receive_backup` to util (#132271)

* Extract file receiver from BackupManager.async_receive_backup to util

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Make sure backup directory exists (#132269)

* Make sure backup directory exists

* Hand off directory creation to executor

* Use mkdir's exist_ok feeature

* Organize BackupManager instance attributes (#132277)

* Don't store received backups in a TempDir (#132272)

* Don't store received backups in a TempDir

* Fix tests

* Make sure backup directory exists

* Address review comments

* Fix tests

* Rewrite backup manager state handling (#132375)

* Rewrite backup manager state handling

* Address review comments

* Modify backup reader/writer API to be stream oriented (#132464)

* Internalize backup tasks (#132482)

* Internalize backup tasks

* Update test after rebase

* Handle backup error during automatic backup (#132511)

* Improve backup manager state logging (#132549)

* Fix backup manager state when restore completes (#132548)

* Remove WS command backup/agents/download (#132664)

* Add WS command backup/generate_with_stored_settings (#132671)

* Add WS command backup/generate_with_stored_settings

* Register the new command, add tests

* Refactor local agent backup tests (#132683)

* Refactor test_load_backups

* Refactor test loading agents

* Refactor test_delete_backup

* Refactor test_upload

* Clean up duplicate tests

* Refactor backup manager receive tests (#132701)

* Refactor backup manager receive tests

* Clean up

* Refactor pre and post platform tests (#132708)

* Refactor backup pre platform test

* Refactor backup post platform test

* Bump aiohasupervisor to version 0.2.2b0 (#132704)

* Bump aiohasupervisor to version 0.2.2b0

* Adjust tests

* Publish event when manager is idle after creating backup (#132724)

* Handle busy backup manager when uploading backup (#132736)

* Adjust hassio backup agent to supervisor changes (#132732)

* Adjust hassio backup agent to supervisor changes

* Fix typo

* Refactor test for create backup with wrong parameters (#132763)

* Refactor test not loading bad backup platforms (#132769)

* Improve receive backup coverage (#132758)

* Refactor initiate backup test (#132829)

* Rename Backup to ManagerBackup (#132841)

* Refactor backup config (#132845)

* Refactor backup config

* Remove unnecessary condition

* Adjust tests

* Improve initiate backup test (#132858)

* Store the time of automatic backup attempts (#132860)

* Store the time of automatic backup attempts

* Address review comments

* Update test

* Update cloud test

* Save agent failures when creating backups (#132850)

* Save agent failures when creating backups

* Update tests

* Store KnownBackups

* Add test

* Only clear known_backups on no error, add tests

* Address review comments

* Store known backups as a list

* Update tests

* Track all backups created with backup strategy settings (#132916)

* Track all backups created with saved settings

* Rename

* Add explicit call to save the store

* Don't register service backup.create in HassOS installations (#132932)

* Revert changes to action service backup.create (#132938)

* Fix logic for cleaning up temporary backup file (#132934)

* Fix logic for cleaning up temporary backup file

* Reduce scope of patch

* Fix with_strategy_settings info not sent over websocket (#132939)

* Fix with_strategy_settings info not sent over websocket

* Fix kitchen sink tests

* Fix cloud and hassio tests

* Revert backup ci changes (#132955)

Revert changes speeding up CI

* Fix revert of CI changes (#132960)

---------

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: Krisjanis Lejejs <krisjanis.lejejs@gmail.com>
2024-12-11 21:49:34 +01:00

1754 lines
55 KiB
Python

"""Tests for the Backup integration."""
from asyncio import Future
from collections.abc import Generator
from datetime import datetime
from typing import Any
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.backup import AgentBackup, BackupAgentError
from homeassistant.components.backup.agent import BackupAgentUnreachableError
from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN
from homeassistant.components.backup.manager import (
CreateBackupEvent,
CreateBackupState,
NewBackup,
WrittenBackup,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .common import (
LOCAL_AGENT_ID,
TEST_BACKUP_ABC123,
TEST_BACKUP_DEF456,
BackupAgentTest,
setup_backup_integration,
)
from tests.common import async_fire_time_changed, async_mock_service
from tests.typing import WebSocketGenerator
BACKUP_CALL = call(
agent_ids=["test.test-agent"],
backup_name="test-name",
include_addons=["test-addon"],
include_all_addons=False,
include_database=True,
include_folders=["media"],
include_homeassistant=True,
password="test-password",
on_progress=ANY,
)
DEFAULT_STORAGE_DATA = {
"backups": {},
"config": {
"create_backup": {
"agent_ids": [],
"include_addons": None,
"include_all_addons": False,
"include_database": True,
"include_folders": None,
"name": None,
"password": None,
},
"last_attempted_strategy_backup": None,
"last_completed_strategy_backup": None,
"retention": {
"copies": None,
"days": None,
},
"schedule": {
"state": "never",
},
},
}
@pytest.fixture
def sync_access_token_proxy(
access_token_fixture_name: str,
request: pytest.FixtureRequest,
) -> str:
"""Non-async proxy for the *_access_token fixture.
Workaround for https://github.com/pytest-dev/pytest-asyncio/issues/112
"""
return request.getfixturevalue(access_token_fixture_name)
@pytest.fixture(autouse=True)
def mock_delay_save() -> Generator[None]:
"""Mock the delay save constant."""
with patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0):
yield
@pytest.fixture(name="create_backup")
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.open_stream = AsyncMock()
mock_written_backup.release_stream = AsyncMock()
fut = Future()
fut.set_result(mock_written_backup)
with patch(
"homeassistant.components.backup.CoreBackupReaderWriter.async_create_backup"
) as mock_create_backup:
mock_create_backup.return_value = (MagicMock(), fut)
yield mock_create_backup
@pytest.fixture(name="delete_backup")
def mock_delete_backup() -> Generator[AsyncMock]:
"""Mock manager delete backup."""
with patch(
"homeassistant.components.backup.BackupManager.async_delete_backup"
) as mock_delete_backup:
yield mock_delete_backup
@pytest.fixture(name="get_backups")
def mock_get_backups() -> Generator[AsyncMock]:
"""Mock manager get backups."""
with patch(
"homeassistant.components.backup.BackupManager.async_get_backups"
) as mock_get_backups:
yield mock_get_backups
@pytest.mark.parametrize(
("remote_agents", "remote_backups"),
[
([], {}),
(["remote"], {}),
(["remote"], {"test.remote": [TEST_BACKUP_ABC123]}),
(["remote"], {"test.remote": [TEST_BACKUP_DEF456]}),
],
)
async def test_info(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
remote_agents: list[str],
remote_backups: dict[str, list[AgentBackup]],
snapshot: SnapshotAssertion,
) -> None:
"""Test getting backup info."""
await setup_backup_integration(
hass,
with_hassio=False,
backups={LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]} | remote_backups,
remote_agents=remote_agents,
)
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json_auto_id({"type": "backup/info"})
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"side_effect", [HomeAssistantError("Boom!"), BackupAgentUnreachableError]
)
async def test_info_with_errors(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
side_effect: Exception,
snapshot: SnapshotAssertion,
) -> None:
"""Test getting backup info with one unavailable agent."""
await setup_backup_integration(
hass, with_hassio=False, backups={LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]}
)
hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest("test")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch.object(BackupAgentTest, "async_list_backups", side_effect=side_effect):
await client.send_json_auto_id({"type": "backup/info"})
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
("remote_agents", "backups"),
[
([], {}),
(["remote"], {LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]}),
(["remote"], {"test.remote": [TEST_BACKUP_ABC123]}),
(["remote"], {"test.remote": [TEST_BACKUP_DEF456]}),
(
["remote"],
{
LOCAL_AGENT_ID: [TEST_BACKUP_ABC123],
"test.remote": [TEST_BACKUP_ABC123],
},
),
],
)
async def test_details(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
remote_agents: list[str],
backups: dict[str, list[AgentBackup]],
snapshot: SnapshotAssertion,
) -> None:
"""Test getting backup info."""
await setup_backup_integration(
hass, with_hassio=False, backups=backups, remote_agents=remote_agents
)
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch("pathlib.Path.exists", return_value=True):
await client.send_json_auto_id(
{"type": "backup/details", "backup_id": "abc123"}
)
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"side_effect", [HomeAssistantError("Boom!"), BackupAgentUnreachableError]
)
async def test_details_with_errors(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
side_effect: Exception,
snapshot: SnapshotAssertion,
) -> None:
"""Test getting backup info with one unavailable agent."""
await setup_backup_integration(
hass, with_hassio=False, backups={LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]}
)
hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest("test")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with (
patch("pathlib.Path.exists", return_value=True),
patch.object(BackupAgentTest, "async_get_backup", side_effect=side_effect),
):
await client.send_json_auto_id(
{"type": "backup/details", "backup_id": "abc123"}
)
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
("remote_agents", "backups"),
[
([], {}),
(["remote"], {LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]}),
(["remote"], {"test.remote": [TEST_BACKUP_ABC123]}),
(["remote"], {"test.remote": [TEST_BACKUP_DEF456]}),
(
["remote"],
{
LOCAL_AGENT_ID: [TEST_BACKUP_ABC123],
"test.remote": [TEST_BACKUP_ABC123],
},
),
],
)
async def test_delete(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
remote_agents: list[str],
backups: dict[str, list[AgentBackup]],
snapshot: SnapshotAssertion,
) -> None:
"""Test deleting a backup file."""
await setup_backup_integration(
hass, with_hassio=False, backups=backups, remote_agents=remote_agents
)
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json_auto_id({"type": "backup/info"})
assert await client.receive_json() == snapshot
await client.send_json_auto_id({"type": "backup/delete", "backup_id": "abc123"})
assert await client.receive_json() == snapshot
await client.send_json_auto_id({"type": "backup/info"})
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"storage_data",
[
DEFAULT_STORAGE_DATA,
DEFAULT_STORAGE_DATA
| {
"backups": [
{
"backup_id": "abc123",
"failed_agent_ids": ["test.remote"],
"with_strategy_settings": False,
}
]
},
],
)
@pytest.mark.parametrize(
"side_effect", [None, HomeAssistantError("Boom!"), BackupAgentUnreachableError]
)
async def test_delete_with_errors(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
hass_storage: dict[str, Any],
side_effect: Exception,
storage_data: dict[str, Any] | None,
snapshot: SnapshotAssertion,
) -> None:
"""Test deleting a backup with one unavailable agent."""
hass_storage[DOMAIN] = {
"data": storage_data,
"key": DOMAIN,
"version": 1,
}
await setup_backup_integration(
hass, with_hassio=False, backups={LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]}
)
hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest("test")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch.object(BackupAgentTest, "async_delete_backup", side_effect=side_effect):
await client.send_json_auto_id({"type": "backup/delete", "backup_id": "abc123"})
assert await client.receive_json() == snapshot
await client.send_json_auto_id({"type": "backup/info"})
assert await client.receive_json() == snapshot
async def test_agent_delete_backup(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test deleting a backup file with a mock agent."""
await setup_backup_integration(hass)
hass.data[DATA_MANAGER].backup_agents = {"domain.test": BackupAgentTest("test")}
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch.object(BackupAgentTest, "async_delete_backup") as delete_mock:
await client.send_json_auto_id(
{
"type": "backup/delete",
"backup_id": "abc123",
}
)
assert await client.receive_json() == snapshot
assert delete_mock.call_args == call("abc123")
@pytest.mark.parametrize(
"data",
[
None,
{},
{"password": "abc123"},
],
)
@pytest.mark.usefixtures("mock_backup_generation")
async def test_generate(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
data: dict[str, Any] | None,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Test generating a backup."""
await setup_backup_integration(hass, with_hassio=False)
client = await hass_ws_client(hass)
freezer.move_to("2024-11-13 12:01:00+01:00")
await hass.async_block_till_done()
await client.send_json_auto_id({"type": "backup/subscribe_events"})
assert await client.receive_json() == snapshot
await client.send_json_auto_id(
{"type": "backup/generate", **{"agent_ids": ["backup.local"]} | (data or {})}
)
for _ in range(6):
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
("parameters", "expected_error"),
[
(
{"include_homeassistant": False},
"Home Assistant must be included in backup",
),
(
{"include_addons": ["blah"]},
"Addons and folders are not supported by core backup",
),
(
{"include_all_addons": True},
"Addons and folders are not supported by core backup",
),
(
{"include_folders": ["ssl"]},
"Addons and folders are not supported by core backup",
),
],
)
async def test_generate_wrong_parameters(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
parameters: dict[str, Any],
expected_error: str,
) -> None:
"""Test generating a backup."""
await setup_backup_integration(hass, with_hassio=False)
client = await hass_ws_client(hass)
default_parameters = {"type": "backup/generate", "agent_ids": ["backup.local"]}
await client.send_json_auto_id(default_parameters | parameters)
response = await client.receive_json()
assert not response["success"]
assert response["error"] == {
"code": "home_assistant_error",
"message": expected_error,
}
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize(
("params", "expected_extra_call_params"),
[
({"agent_ids": ["backup.local"]}, {"agent_ids": ["backup.local"]}),
(
{
"agent_ids": ["backup.local"],
"include_database": False,
"name": "abc123",
},
{
"agent_ids": ["backup.local"],
"include_addons": None,
"include_database": False,
"include_folders": None,
"name": "abc123",
},
),
],
)
async def test_generate_calls_create(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
params: dict[str, Any],
expected_extra_call_params: dict[str, Any],
) -> None:
"""Test translation of WS parameter to backup/generate to async_initiate_backup."""
await setup_backup_integration(hass, with_hassio=False)
client = await hass_ws_client(hass)
freezer.move_to("2024-11-13 12:01:00+01:00")
await hass.async_block_till_done()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_initiate_backup",
return_value=NewBackup(backup_job_id="abc123"),
) as generate_backup:
await client.send_json_auto_id({"type": "backup/generate"} | params)
result = await client.receive_json()
assert result["success"]
assert result["result"] == {"backup_job_id": "abc123"}
generate_backup.assert_called_once_with(
**{
"include_all_addons": False,
"include_homeassistant": True,
"include_addons": None,
"include_database": True,
"include_folders": None,
"name": None,
"password": None,
}
| expected_extra_call_params
)
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize(
("create_backup_settings", "expected_call_params"),
[
(
{},
{
"agent_ids": [],
"include_addons": None,
"include_all_addons": False,
"include_database": True,
"include_folders": None,
"include_homeassistant": True,
"name": None,
"password": None,
"with_strategy_settings": True,
},
),
(
{
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_all_addons": False,
"include_database": True,
"include_folders": ["media"],
"name": "test-name",
"password": "test-password",
},
{
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_all_addons": False,
"include_database": True,
"include_folders": ["media"],
"include_homeassistant": True,
"name": "test-name",
"password": "test-password",
"with_strategy_settings": True,
},
),
],
)
async def test_generate_with_default_settings_calls_create(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
create_backup_settings: dict[str, Any],
expected_call_params: dict[str, Any],
) -> None:
"""Test backup/generate_with_strategy_settings calls async_initiate_backup."""
await setup_backup_integration(hass, with_hassio=False)
client = await hass_ws_client(hass)
freezer.move_to("2024-11-13 12:01:00+01:00")
await hass.async_block_till_done()
await client.send_json_auto_id(
{"type": "backup/config/update", "create_backup": create_backup_settings}
)
result = await client.receive_json()
assert result["success"]
with patch(
"homeassistant.components.backup.manager.BackupManager.async_initiate_backup",
return_value=NewBackup(backup_job_id="abc123"),
) as generate_backup:
await client.send_json_auto_id(
{"type": "backup/generate_with_strategy_settings"}
)
result = await client.receive_json()
assert result["success"]
assert result["result"] == {"backup_job_id": "abc123"}
generate_backup.assert_called_once_with(**expected_call_params)
@pytest.mark.parametrize(
"backups",
[
{},
{LOCAL_AGENT_ID: [TEST_BACKUP_ABC123]},
],
)
async def test_restore_local_agent(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
backups: dict[str, list[AgentBackup]],
snapshot: SnapshotAssertion,
) -> None:
"""Test calling the restore command."""
await setup_backup_integration(hass, with_hassio=False, backups=backups)
restart_calls = async_mock_service(hass, "homeassistant", "restart")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with (
patch("pathlib.Path.exists", return_value=True),
patch("pathlib.Path.write_text"),
):
await client.send_json_auto_id(
{
"type": "backup/restore",
"backup_id": "abc123",
"agent_id": "backup.local",
}
)
assert await client.receive_json() == snapshot
assert len(restart_calls) == snapshot
@pytest.mark.parametrize(
("remote_agents", "backups"),
[
(["remote"], {}),
(["remote"], {"test.remote": [TEST_BACKUP_ABC123]}),
],
)
async def test_restore_remote_agent(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
remote_agents: list[str],
backups: dict[str, list[AgentBackup]],
snapshot: SnapshotAssertion,
) -> None:
"""Test calling the restore command."""
await setup_backup_integration(
hass, with_hassio=False, backups=backups, remote_agents=remote_agents
)
restart_calls = async_mock_service(hass, "homeassistant", "restart")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
with patch("pathlib.Path.write_text"), patch("pathlib.Path.open"):
await client.send_json_auto_id(
{
"type": "backup/restore",
"backup_id": "abc123",
"agent_id": "test.remote",
}
)
assert await client.receive_json() == snapshot
assert len(restart_calls) == snapshot
@pytest.mark.parametrize(
"access_token_fixture_name",
["hass_access_token", "hass_supervisor_access_token"],
)
@pytest.mark.parametrize(
("with_hassio"),
[
pytest.param(True, id="with_hassio"),
pytest.param(False, id="without_hassio"),
],
)
@pytest.mark.usefixtures("supervisor_client")
async def test_backup_end(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
sync_access_token_proxy: str,
*,
access_token_fixture_name: str,
with_hassio: bool,
) -> None:
"""Test handling of post backup actions from a WS command."""
await setup_backup_integration(hass, with_hassio=with_hassio)
client = await hass_ws_client(hass, sync_access_token_proxy)
await hass.async_block_till_done()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_post_backup_actions",
):
await client.send_json_auto_id({"type": "backup/end"})
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"access_token_fixture_name",
["hass_access_token", "hass_supervisor_access_token"],
)
@pytest.mark.parametrize(
("with_hassio"),
[
pytest.param(True, id="with_hassio"),
pytest.param(False, id="without_hassio"),
],
)
@pytest.mark.usefixtures("supervisor_client")
async def test_backup_start(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
sync_access_token_proxy: str,
*,
access_token_fixture_name: str,
with_hassio: bool,
) -> None:
"""Test handling of pre backup actions from a WS command."""
await setup_backup_integration(hass, with_hassio=with_hassio)
client = await hass_ws_client(hass, sync_access_token_proxy)
await hass.async_block_till_done()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_pre_backup_actions",
):
await client.send_json_auto_id({"type": "backup/start"})
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"exception",
[
TimeoutError(),
HomeAssistantError("Boom"),
Exception("Boom"),
],
)
@pytest.mark.usefixtures("supervisor_client")
async def test_backup_end_exception(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
hass_supervisor_access_token: str,
exception: Exception,
) -> None:
"""Test exception handling while running post backup actions from a WS command."""
await setup_backup_integration(hass, with_hassio=True)
client = await hass_ws_client(hass, hass_supervisor_access_token)
await hass.async_block_till_done()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_post_backup_actions",
side_effect=exception,
):
await client.send_json_auto_id({"type": "backup/end"})
assert await client.receive_json() == snapshot
@pytest.mark.parametrize(
"exception",
[
TimeoutError(),
HomeAssistantError("Boom"),
Exception("Boom"),
],
)
@pytest.mark.usefixtures("supervisor_client")
async def test_backup_start_exception(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
hass_supervisor_access_token: str,
exception: Exception,
) -> None:
"""Test exception handling while running pre backup actions from a WS command."""
await setup_backup_integration(hass, with_hassio=True)
client = await hass_ws_client(hass, hass_supervisor_access_token)
await hass.async_block_till_done()
with patch(
"homeassistant.components.backup.manager.BackupManager.async_pre_backup_actions",
side_effect=exception,
):
await client.send_json_auto_id({"type": "backup/start"})
assert await client.receive_json() == snapshot
async def test_agents_info(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test getting backup agents info."""
await setup_backup_integration(hass, with_hassio=False)
hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest("test")
client = await hass_ws_client(hass)
await hass.async_block_till_done()
await client.send_json_auto_id({"type": "backup/agents/info"})
assert await client.receive_json() == snapshot
@pytest.mark.usefixtures("create_backup", "delete_backup", "get_backups")
@pytest.mark.parametrize(
"storage_data",
[
None,
{
"backups": {},
"config": {
"create_backup": {
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_all_addons": True,
"include_database": True,
"include_folders": ["media"],
"name": "test-name",
"password": "test-password",
},
"retention": {"copies": 3, "days": 7},
"last_attempted_strategy_backup": datetime.fromisoformat(
"2024-10-26T04:45:00+01:00"
),
"last_completed_strategy_backup": datetime.fromisoformat(
"2024-10-26T04:45:00+01:00"
),
"schedule": {"state": "daily"},
},
},
{
"backups": {},
"config": {
"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": 3, "days": None},
"last_attempted_strategy_backup": None,
"last_completed_strategy_backup": None,
"schedule": {"state": "never"},
},
},
{
"backups": {},
"config": {
"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": 7},
"last_attempted_strategy_backup": datetime.fromisoformat(
"2024-10-27T04:45:00+01:00"
),
"last_completed_strategy_backup": datetime.fromisoformat(
"2024-10-26T04:45:00+01:00"
),
"schedule": {"state": "never"},
},
},
{
"backups": {},
"config": {
"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_strategy_backup": None,
"last_completed_strategy_backup": None,
"schedule": {"state": "mon"},
},
},
{
"backups": {},
"config": {
"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_strategy_backup": None,
"last_completed_strategy_backup": None,
"schedule": {"state": "sat"},
},
},
],
)
async def test_config_info(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
hass_storage: dict[str, Any],
storage_data: dict[str, Any] | None,
) -> None:
"""Test getting backup config info."""
hass_storage[DOMAIN] = {
"data": storage_data,
"key": DOMAIN,
"version": 1,
}
await setup_backup_integration(hass)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/config/info"})
assert await client.receive_json() == snapshot
@pytest.mark.usefixtures("create_backup", "delete_backup", "get_backups")
@pytest.mark.parametrize(
"command",
[
{
"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": "daily",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"schedule": "mon",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"schedule": "never",
},
{
"type": "backup/config/update",
"create_backup": {
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_folders": ["media"],
"name": "test-name",
"password": "test-password",
},
"schedule": "daily",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": 3, "days": 7},
"schedule": "daily",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": None},
"schedule": "daily",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": 3, "days": None},
"schedule": "daily",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 7},
"schedule": "daily",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": 3},
"schedule": "daily",
},
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"days": 7},
"schedule": "daily",
},
],
)
async def test_config_update(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
command: dict[str, Any],
hass_storage: dict[str, Any],
) -> None:
"""Test updating the backup config."""
await setup_backup_integration(hass)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
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()
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()
assert hass_storage[DOMAIN] == snapshot
@pytest.mark.usefixtures("create_backup", "delete_backup", "get_backups")
@pytest.mark.parametrize(
"command",
[
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"schedule": "someday",
},
],
)
async def test_config_update_errors(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
command: dict[str, Any],
) -> None:
"""Test errors when updating the backup config."""
await setup_backup_integration(hass)
await hass.async_block_till_done()
client = await hass_ws_client(hass)
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()
assert not result["success"]
await client.send_json_auto_id({"type": "backup/config/info"})
assert await client.receive_json() == snapshot
await hass.async_block_till_done()
@pytest.mark.parametrize(
(
"command",
"last_completed_strategy_backup",
"time_1",
"time_2",
"attempted_backup_time",
"completed_backup_time",
"backup_calls_1",
"backup_calls_2",
"call_args",
"create_backup_side_effect",
),
[
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "daily",
},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-13T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
2,
BACKUP_CALL,
None,
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "mon",
},
"2024-11-11T04:45:00+01:00",
"2024-11-18T04:45:00+01:00",
"2024-11-25T04:45:00+01:00",
"2024-11-18T04:45:00+01:00",
"2024-11-18T04:45:00+01:00",
1,
2,
BACKUP_CALL,
None,
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "never",
},
"2024-11-11T04:45:00+01:00",
"2034-11-11T12:00:00+01:00", # ten years later and still no backups
"2034-11-11T13:00:00+01:00",
"2024-11-11T04:45:00+01:00",
"2024-11-11T04:45:00+01:00",
0,
0,
None,
None,
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "daily",
},
"2024-10-26T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-13T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
2,
BACKUP_CALL,
None,
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "mon",
},
"2024-10-26T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-13T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", # missed event uses daily schedule once
"2024-11-12T04:45:00+01:00", # missed event uses daily schedule once
1,
1,
BACKUP_CALL,
None,
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "never",
},
"2024-10-26T04:45:00+01:00",
"2034-11-11T12:00:00+01:00", # ten years later and still no backups
"2034-11-12T12:00:00+01:00",
"2024-10-26T04:45:00+01:00",
"2024-10-26T04:45:00+01:00",
0,
0,
None,
None,
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"schedule": "daily",
},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-13T04:45:00+01:00",
"2024-11-12T04:45:00+01:00", # attempted to create backup but failed
"2024-11-11T04:45:00+01:00",
1,
2,
BACKUP_CALL,
[Exception("Boom"), None],
),
],
)
async def test_config_schedule_logic(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
hass_storage: dict[str, Any],
create_backup: AsyncMock,
command: dict[str, Any],
last_completed_strategy_backup: str,
time_1: str,
time_2: str,
attempted_backup_time: str,
completed_backup_time: str,
backup_calls_1: int,
backup_calls_2: int,
call_args: Any,
create_backup_side_effect: list[Exception | None] | None,
) -> None:
"""Test config schedule logic."""
client = await hass_ws_client(hass)
storage_data = {
"backups": {},
"config": {
"create_backup": {
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_all_addons": False,
"include_database": True,
"include_folders": ["media"],
"name": "test-name",
"password": "test-password",
},
"retention": {"copies": None, "days": None},
"last_attempted_strategy_backup": datetime.fromisoformat(
last_completed_strategy_backup
),
"last_completed_strategy_backup": datetime.fromisoformat(
last_completed_strategy_backup
),
"schedule": {"state": "daily"},
},
}
hass_storage[DOMAIN] = {
"data": storage_data,
"key": DOMAIN,
"version": 1,
}
create_backup.side_effect = create_backup_side_effect
await hass.config.async_set_time_zone("Europe/Amsterdam")
freezer.move_to("2024-11-11 12:00:00+01:00")
await setup_backup_integration(hass, remote_agents=["test-agent"])
await hass.async_block_till_done()
await client.send_json_auto_id(command)
result = await client.receive_json()
assert result["success"]
freezer.move_to(time_1)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert create_backup.call_count == backup_calls_1
assert create_backup.call_args == call_args
async_fire_time_changed(hass, fire_all=True) # flush out storage save
await hass.async_block_till_done()
assert (
hass_storage[DOMAIN]["data"]["config"]["last_attempted_strategy_backup"]
== attempted_backup_time
)
assert (
hass_storage[DOMAIN]["data"]["config"]["last_completed_strategy_backup"]
== completed_backup_time
)
freezer.move_to(time_2)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert create_backup.call_count == backup_calls_2
assert create_backup.call_args == call_args
@pytest.mark.parametrize(
(
"command",
"backups",
"get_backups_agent_errors",
"delete_backup_agent_errors",
"last_backup_time",
"next_time",
"backup_time",
"backup_calls",
"get_backups_calls",
"delete_calls",
"delete_args_list",
),
[
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": None, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1, # we get backups even if backup retention copies is None
0,
[],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1,
0,
[],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-09T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-11T04:45:00+01:00"),
"backup-4": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1,
1,
[call("backup-1")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-09T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-11T04:45:00+01:00"),
"backup-4": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1,
2,
[call("backup-1"), call("backup-2")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{"test-agent": BackupAgentError("Boom!")},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1,
1,
[call("backup-1")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{},
{"test-agent": BackupAgentError("Boom!")},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1,
1,
[call("backup-1")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 0, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-09T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-11T04:45:00+01:00"),
"backup-4": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1,
3,
[call("backup-1"), call("backup-2"), call("backup-3")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 0, "days": None},
"schedule": "daily",
},
{
"backup-1": MagicMock(date="2024-11-12T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
"2024-11-12T04:45:00+01:00",
1,
1,
0,
[],
),
],
)
async def test_config_retention_copies_logic(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
hass_storage: dict[str, Any],
create_backup: AsyncMock,
delete_backup: AsyncMock,
get_backups: AsyncMock,
command: dict[str, Any],
backups: dict[str, Any],
get_backups_agent_errors: dict[str, Exception],
delete_backup_agent_errors: dict[str, Exception],
last_backup_time: str,
next_time: str,
backup_time: str,
backup_calls: int,
get_backups_calls: int,
delete_calls: int,
delete_args_list: Any,
) -> None:
"""Test config backup retention copies logic."""
client = await hass_ws_client(hass)
storage_data = {
"backups": {},
"config": {
"create_backup": {
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_all_addons": False,
"include_database": True,
"include_folders": ["media"],
"name": "test-name",
"password": "test-password",
},
"retention": {"copies": None, "days": None},
"last_attempted_strategy_backup": None,
"last_completed_strategy_backup": datetime.fromisoformat(last_backup_time),
"schedule": {"state": "daily"},
},
}
hass_storage[DOMAIN] = {
"data": storage_data,
"key": DOMAIN,
"version": 1,
}
get_backups.return_value = (backups, get_backups_agent_errors)
delete_backup.return_value = delete_backup_agent_errors
await hass.config.async_set_time_zone("Europe/Amsterdam")
freezer.move_to("2024-11-11 12:00:00+01:00")
await setup_backup_integration(hass, remote_agents=["test-agent"])
await hass.async_block_till_done()
await client.send_json_auto_id(command)
result = await client.receive_json()
assert result["success"]
freezer.move_to(next_time)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert create_backup.call_count == backup_calls
assert get_backups.call_count == get_backups_calls
assert delete_backup.call_count == delete_calls
assert delete_backup.call_args_list == delete_args_list
async_fire_time_changed(hass, fire_all=True) # flush out storage save
await hass.async_block_till_done()
assert (
hass_storage[DOMAIN]["data"]["config"]["last_attempted_strategy_backup"]
== backup_time
)
assert (
hass_storage[DOMAIN]["data"]["config"]["last_completed_strategy_backup"]
== backup_time
)
@pytest.mark.parametrize(
(
"command",
"backups",
"get_backups_agent_errors",
"delete_backup_agent_errors",
"last_backup_time",
"start_time",
"next_time",
"get_backups_calls",
"delete_calls",
"delete_args_list",
),
[
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2},
"schedule": "never",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-11T12:00:00+01:00",
"2024-11-12T12:00:00+01:00",
1,
1,
[call("backup-1")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 3},
"schedule": "never",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-11T12:00:00+01:00",
"2024-11-12T12:00:00+01:00",
1,
0,
[],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2},
"schedule": "never",
},
{
"backup-1": MagicMock(date="2024-11-09T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-11T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-11T12:00:00+01:00",
"2024-11-12T12:00:00+01:00",
1,
2,
[call("backup-1"), call("backup-2")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2},
"schedule": "never",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
},
{"test-agent": BackupAgentError("Boom!")},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-11T12:00:00+01:00",
"2024-11-12T12:00:00+01:00",
1,
1,
[call("backup-1")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 2},
"schedule": "never",
},
{
"backup-1": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-11T04:45:00+01:00"),
},
{},
{"test-agent": BackupAgentError("Boom!")},
"2024-11-11T04:45:00+01:00",
"2024-11-11T12:00:00+01:00",
"2024-11-12T12:00:00+01:00",
1,
1,
[call("backup-1")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 0},
"schedule": "never",
},
{
"backup-1": MagicMock(date="2024-11-09T04:45:00+01:00"),
"backup-2": MagicMock(date="2024-11-10T04:45:00+01:00"),
"backup-3": MagicMock(date="2024-11-11T04:45:00+01:00"),
},
{},
{},
"2024-11-11T04:45:00+01:00",
"2024-11-11T12:00:00+01:00",
"2024-11-12T12:00:00+01:00",
1,
2,
[call("backup-1"), call("backup-2")],
),
],
)
async def test_config_retention_days_logic(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
hass_storage: dict[str, Any],
delete_backup: AsyncMock,
get_backups: AsyncMock,
command: dict[str, Any],
backups: dict[str, Any],
get_backups_agent_errors: dict[str, Exception],
delete_backup_agent_errors: dict[str, Exception],
last_backup_time: str,
start_time: str,
next_time: str,
get_backups_calls: int,
delete_calls: int,
delete_args_list: list[Any],
) -> None:
"""Test config backup retention logic."""
client = await hass_ws_client(hass)
storage_data = {
"backups": {},
"config": {
"create_backup": {
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_all_addons": False,
"include_database": True,
"include_folders": ["media"],
"name": "test-name",
"password": "test-password",
},
"retention": {"copies": None, "days": None},
"last_attempted_strategy_backup": None,
"last_completed_strategy_backup": datetime.fromisoformat(last_backup_time),
"schedule": {"state": "never"},
},
}
hass_storage[DOMAIN] = {
"data": storage_data,
"key": DOMAIN,
"version": 1,
}
get_backups.return_value = (backups, get_backups_agent_errors)
delete_backup.return_value = delete_backup_agent_errors
await hass.config.async_set_time_zone("Europe/Amsterdam")
freezer.move_to(start_time)
await setup_backup_integration(hass)
await hass.async_block_till_done()
await client.send_json_auto_id(command)
result = await client.receive_json()
assert result["success"]
freezer.move_to(next_time)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert get_backups.call_count == get_backups_calls
assert delete_backup.call_count == delete_calls
assert delete_backup.call_args_list == delete_args_list
async_fire_time_changed(hass, fire_all=True) # flush out storage save
await hass.async_block_till_done()
async def test_subscribe_event(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test generating a backup."""
await setup_backup_integration(hass, with_hassio=False)
manager = hass.data[DATA_MANAGER]
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/subscribe_events"})
assert await client.receive_json() == snapshot
assert await client.receive_json() == snapshot
manager.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.IN_PROGRESS)
)
assert await client.receive_json() == snapshot