Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MVP implementation of Backup sync agents #126122

Open
wants to merge 26 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions tests/components/backup/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from unittest.mock import patch

from homeassistant.components.backup import DOMAIN
from homeassistant.components.backup.manager import Backup
from homeassistant.components.backup.models import BackupSyncMetadata
from homeassistant.components.backup.sync_agent import BackupSyncAgent, SyncedBackup
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component
Expand All @@ -20,6 +23,40 @@
)


class BackupSyncAgentTest(BackupSyncAgent):
"""Test backup sync agent."""

async def async_download_backup(
self,
*,
id: str,
path: Path,
**kwargs: Any,
) -> None:
"""Download a backup file."""

async def async_upload_backup(
self,
*,
path: Path,
metadata: BackupSyncMetadata,
**kwargs: Any,
) -> None:
"""Upload a backup."""

async def async_list_backups(self, **kwargs: Any) -> list[SyncedBackup]:
"""List synced backups."""
return [
SyncedBackup(
id="abc123",
name="Test",
slug="abc123",
size=13.37,
date="1970-01-01T00:00:00Z",
)
]


async def setup_backup_integration(
hass: HomeAssistant,
with_hassio: bool = False,
Expand Down
91 changes: 91 additions & 0 deletions tests/components/backup/snapshots/test_websocket.ambr
Original file line number Diff line number Diff line change
@@ -1,4 +1,95 @@
# serializer version: 1
# name: test_agents_download[with_hassio]
_Call(
tuple(
),
dict({
'id': 'abc123',
'path': PosixPath('/workspaces/core/tests/testing_config/backup/abc123.tar'),
}),
)
# ---
# name: test_agents_download[without_hassio]
_Call(
tuple(
),
dict({
'id': 'abc123',
'path': PosixPath('/workspaces/core/tests/testing_config/backup/abc123.tar'),
}),
)
# ---
# name: test_agents_download_exception
dict({
'error': dict({
'code': 'backup_agents_download',
'message': 'Boom',
}),
'id': 1,
'success': False,
'type': 'result',
})
# ---
# name: test_agents_info[with_hassio]
dict({
'id': 1,
'result': dict({
'agents': list([
'domain.test',
]),
'syncing': False,
}),
'success': True,
'type': 'result',
})
# ---
# name: test_agents_info[without_hassio]
dict({
'id': 1,
'result': dict({
'agents': list([
'domain.test',
]),
'syncing': False,
}),
'success': True,
'type': 'result',
})
# ---
# name: test_agents_synced[with_hassio]
dict({
'id': 1,
'result': list([
dict({
'agent_id': 'domain.test',
'date': '1970-01-01T00:00:00Z',
'id': 'abc123',
'name': 'Test',
'size': 13.37,
'slug': 'abc123',
}),
]),
'success': True,
'type': 'result',
})
# ---
# name: test_agents_synced[without_hassio]
dict({
'id': 1,
'result': list([
dict({
'agent_id': 'domain.test',
'date': '1970-01-01T00:00:00Z',
'id': 'abc123',
'name': 'Test',
'size': 13.37,
'slug': 'abc123',
}),
]),
'success': True,
'type': 'result',
})
# ---
# name: test_backup_end[with_hassio-hass_access_token]
dict({
'error': dict({
Expand Down
164 changes: 161 additions & 3 deletions tests/components/backup/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest

from homeassistant.components.backup import BackupManager
from homeassistant.components.backup.manager import BackupPlatformProtocol
from homeassistant.components.backup.sync_agent import BackupPlatformAgentProtocol
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component

from .common import TEST_BACKUP
from .common import TEST_BACKUP, BackupSyncAgentTest

from tests.common import MockPlatform, mock_platform

Expand Down Expand Up @@ -62,7 +64,7 @@ def _mock_iterdir(path: Path) -> list[Path]:
"2025.1.0",
),
):
await manager.generate_backup()
backup = await manager.generate_backup()

assert mocked_json_bytes.call_count == 1
backup_json_dict = mocked_json_bytes.call_args[0][0]
Expand All @@ -71,11 +73,12 @@ def _mock_iterdir(path: Path) -> list[Path]:
assert manager.backup_dir.as_posix() in str(
mocked_tarfile.call_args_list[0][0][0]
)
return backup


async def _setup_mock_domain(
hass: HomeAssistant,
platform: BackupPlatformProtocol | None = None,
platform: BackupPlatformProtocol | BackupPlatformAgentProtocol | None = None,
) -> None:
"""Set up a mock domain."""
mock_platform(hass, "some_domain.backup", platform or MockPlatform())
Expand Down Expand Up @@ -193,6 +196,7 @@ async def test_generate_backup(
assert "Generated new backup with slug " in caplog.text
assert "Creating backup directory" in caplog.text
assert "Loaded 0 platforms" in caplog.text
assert "Loaded 0 agents" in caplog.text


async def test_loading_platforms(
Expand Down Expand Up @@ -222,6 +226,34 @@ async def test_loading_platforms(
assert "Loaded 1 platforms" in caplog.text


async def test_loading_sync_agents(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test loading backup sync agents."""
manager = BackupManager(hass)

assert not manager.loaded_platforms
assert not manager.platforms

await _setup_mock_domain(
hass,
Mock(
async_get_backup_sync_agents=AsyncMock(
return_value=[BackupSyncAgentTest("test")]
),
),
)
await manager.load_platforms()
await hass.async_block_till_done()

assert manager.loaded_platforms
assert len(manager.sync_agents) == 1

assert "Loaded 1 agents" in caplog.text
assert "some_domain.test" in manager.sync_agents


async def test_not_loading_bad_platforms(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
Expand All @@ -242,6 +274,132 @@ async def test_not_loading_bad_platforms(
assert "Loaded 0 platforms" in caplog.text


async def test_syncing_backup(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test syncing a backup."""
manager = BackupManager(hass)

await _setup_mock_domain(
hass,
Mock(
async_pre_backup=AsyncMock(),
async_post_backup=AsyncMock(),
async_get_backup_sync_agents=AsyncMock(
return_value=[
BackupSyncAgentTest("agent1"),
BackupSyncAgentTest("agent2"),
]
),
),
)
await manager.load_platforms()
await hass.async_block_till_done()

backup = await _mock_backup_generation(manager)

with (
patch.object(BackupSyncAgentTest, "async_upload_backup") as mocked_upload,
patch(
"homeassistant.components.backup.manager.HAVERSION",
"2025.1.0",
),
):
await manager.sync_backup(backup=backup)
assert mocked_upload.call_count == 2
first_call = mocked_upload.call_args_list[0]
assert first_call[1]["path"] == backup.path
assert first_call[1]["metadata"] == {
"date": backup.date,
"homeassistant": "2025.1.0",
"name": backup.name,
"size": backup.size,
"slug": backup.slug,
}

assert "Error during backup sync" not in caplog.text


async def test_syncing_backup_with_exception(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test syncing a backup with exception."""
manager = BackupManager(hass)

class ModifiedBackupSyncAgentTest(BackupSyncAgentTest):
async def async_upload_backup(self, **kwargs: Any) -> None:
raise HomeAssistantError("Test exception")

await _setup_mock_domain(
hass,
Mock(
async_pre_backup=AsyncMock(),
async_post_backup=AsyncMock(),
async_get_backup_sync_agents=AsyncMock(
return_value=[
ModifiedBackupSyncAgentTest("agent1"),
ModifiedBackupSyncAgentTest("agent2"),
]
),
),
)
await manager.load_platforms()
await hass.async_block_till_done()

backup = await _mock_backup_generation(manager)

with (
patch.object(
ModifiedBackupSyncAgentTest,
"async_upload_backup",
) as mocked_upload,
patch(
"homeassistant.components.backup.manager.HAVERSION",
"2025.1.0",
),
):
mocked_upload.side_effect = HomeAssistantError("Test exception")
await manager.sync_backup(backup=backup)
assert mocked_upload.call_count == 2
first_call = mocked_upload.call_args_list[0]
assert first_call[1]["path"] == backup.path
assert first_call[1]["metadata"] == {
"date": backup.date,
"homeassistant": "2025.1.0",
"name": backup.name,
"size": backup.size,
"slug": backup.slug,
}

assert "Error during backup sync - Test exception" in caplog.text


async def test_syncing_backup_no_agents(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test syncing a backup with no agents."""
manager = BackupManager(hass)

await _setup_mock_domain(
hass,
Mock(
async_pre_backup=AsyncMock(),
async_post_backup=AsyncMock(),
async_get_backup_sync_agents=AsyncMock(return_value=[]),
),
)
await manager.load_platforms()
await hass.async_block_till_done()

backup = await _mock_backup_generation(manager)
with patch("asyncio.gather") as mocked_gather:
ludeeus marked this conversation as resolved.
Show resolved Hide resolved
await manager.sync_backup(backup=backup)
assert mocked_gather.call_count == 0


async def test_exception_plaform_pre(hass: HomeAssistant) -> None:
"""Test exception in pre step."""
manager = BackupManager(hass)
Expand Down
Loading
Loading