Improve hassio backup agent test coverage (#133426)

This commit is contained in:
Erik Montnemery 2024-12-17 15:08:03 +01:00 committed by GitHub
parent 8b3cd41396
commit 4adfd52dc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 248 additions and 2 deletions

View File

@ -173,6 +173,7 @@ class SupervisorBackupAgent(BackupAgent):
except SupervisorBadRequestError as err: except SupervisorBadRequestError as err:
if err.args[0] != "Backup does not exist": if err.args[0] != "Backup does not exist":
raise raise
_LOGGER.debug("Backup %s does not exist", backup_id)
class SupervisorBackupReaderWriter(BackupReaderWriter): class SupervisorBackupReaderWriter(BackupReaderWriter):

View File

@ -1,13 +1,20 @@
"""Test supervisor backup functionality.""" """Test supervisor backup functionality."""
from collections.abc import AsyncGenerator, Generator from collections.abc import (
AsyncGenerator,
AsyncIterator,
Callable,
Coroutine,
Generator,
)
from dataclasses import replace from dataclasses import replace
from datetime import datetime from datetime import datetime
from io import StringIO from io import StringIO
import os import os
from typing import Any from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, Mock, patch
from aiohasupervisor.exceptions import SupervisorBadRequestError
from aiohasupervisor.models import ( from aiohasupervisor.models import (
backups as supervisor_backups, backups as supervisor_backups,
mounts as supervisor_mounts, mounts as supervisor_mounts,
@ -19,13 +26,17 @@ from homeassistant.components.backup import (
DOMAIN as BACKUP_DOMAIN, DOMAIN as BACKUP_DOMAIN,
AddonInfo, AddonInfo,
AgentBackup, AgentBackup,
BackupAgent,
BackupAgentPlatformProtocol,
Folder, Folder,
) )
from homeassistant.components.hassio.backup import LOCATION_CLOUD_BACKUP
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .test_init import MOCK_ENVIRON from .test_init import MOCK_ENVIRON
from tests.common import mock_platform
from tests.typing import ClientSessionGenerator, WebSocketGenerator from tests.typing import ClientSessionGenerator, WebSocketGenerator
TEST_BACKUP = supervisor_backups.Backup( TEST_BACKUP = supervisor_backups.Backup(
@ -189,6 +200,57 @@ async def setup_integration(
await hass.async_block_till_done() await hass.async_block_till_done()
class BackupAgentTest(BackupAgent):
"""Test backup agent."""
domain = "test"
def __init__(self, name: str) -> None:
"""Initialize the backup agent."""
self.name = name
async def async_download_backup(
self, backup_id: str, **kwargs: Any
) -> AsyncIterator[bytes]:
"""Download a backup file."""
return AsyncMock(spec_set=["__aiter__"])
async def async_upload_backup(
self,
*,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
backup: AgentBackup,
**kwargs: Any,
) -> None:
"""Upload a backup."""
await open_stream()
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
"""List backups."""
return []
async def async_get_backup(
self, backup_id: str, **kwargs: Any
) -> AgentBackup | None:
"""Return a backup."""
return None
async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
"""Delete a backup file."""
async def _setup_backup_platform(
hass: HomeAssistant,
*,
domain: str,
platform: BackupAgentPlatformProtocol,
) -> None:
"""Set up a mock domain."""
mock_platform(hass, f"{domain}.backup", platform)
assert await async_setup_component(hass, domain, {})
await hass.async_block_till_done()
@pytest.mark.usefixtures("hassio_client") @pytest.mark.usefixtures("hassio_client")
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mounts", "expected_agents"), ("mounts", "expected_agents"),
@ -405,6 +467,8 @@ async def test_agent_upload(
assert resp.status == 201 assert resp.status == 201
supervisor_client.backups.reload.assert_not_called() supervisor_client.backups.reload.assert_not_called()
supervisor_client.backups.download_backup.assert_not_called()
supervisor_client.backups.remove_backup.assert_not_called()
@pytest.mark.usefixtures("hassio_client", "setup_integration") @pytest.mark.usefixtures("hassio_client", "setup_integration")
@ -430,6 +494,50 @@ async def test_agent_delete_backup(
supervisor_client.backups.remove_backup.assert_called_once_with(backup_id) supervisor_client.backups.remove_backup.assert_called_once_with(backup_id)
@pytest.mark.usefixtures("hassio_client", "setup_integration")
@pytest.mark.parametrize(
("remove_side_effect", "expected_response"),
[
(
SupervisorBadRequestError("blah"),
{
"success": False,
"error": {"code": "unknown_error", "message": "Unknown error"},
},
),
(
SupervisorBadRequestError("Backup does not exist"),
{
"success": True,
"result": {"agent_errors": {}},
},
),
],
)
async def test_agent_delete_with_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
remove_side_effect: Exception,
expected_response: dict[str, Any],
) -> None:
"""Test agent delete backup."""
client = await hass_ws_client(hass)
backup_id = "abc123"
supervisor_client.backups.remove_backup.side_effect = remove_side_effect
await client.send_json_auto_id(
{
"type": "backup/delete",
"backup_id": backup_id,
}
)
response = await client.receive_json()
assert response == {"id": 1, "type": "result"} | expected_response
supervisor_client.backups.remove_backup.assert_called_once_with(backup_id)
@pytest.mark.usefixtures("hassio_client", "setup_integration") @pytest.mark.usefixtures("hassio_client", "setup_integration")
@pytest.mark.parametrize( @pytest.mark.parametrize(
("event_data", "mount_info_calls"), ("event_data", "mount_info_calls"),
@ -598,6 +706,84 @@ async def test_reader_writer_create(
"state": "completed", "state": "completed",
} }
supervisor_client.backups.download_backup.assert_not_called()
supervisor_client.backups.remove_backup.assert_not_called()
@pytest.mark.usefixtures("hassio_client", "setup_integration")
async def test_reader_writer_create_remote_backup(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test generating a backup which will be uploaded to a remote agent."""
client = await hass_ws_client(hass)
supervisor_client.backups.partial_backup.return_value.job_id = "abc123"
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
remote_agent = BackupAgentTest("remote")
await _setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
await client.send_json_auto_id({"type": "backup/subscribe_events"})
response = await client.receive_json()
assert response["event"] == {"manager_state": "idle"}
response = await client.receive_json()
assert response["success"]
await client.send_json_auto_id(
{"type": "backup/generate", "agent_ids": ["test.remote"], "name": "Test"}
)
response = await client.receive_json()
assert response["event"] == {
"manager_state": "create_backup",
"stage": None,
"state": "in_progress",
}
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"backup_job_id": "abc123"}
supervisor_client.backups.partial_backup.assert_called_once_with(
replace(DEFAULT_BACKUP_OPTIONS, location=LOCATION_CLOUD_BACKUP),
)
await client.send_json_auto_id(
{
"type": "supervisor/event",
"data": {
"event": "job",
"data": {"done": True, "uuid": "abc123", "reference": "test_slug"},
},
}
)
response = await client.receive_json()
assert response["success"]
response = await client.receive_json()
assert response["event"] == {
"manager_state": "create_backup",
"stage": "upload_to_agents",
"state": "in_progress",
}
response = await client.receive_json()
assert response["event"] == {
"manager_state": "create_backup",
"stage": None,
"state": "completed",
}
supervisor_client.backups.download_backup.assert_called_once_with("test_slug")
supervisor_client.backups.remove_backup.assert_called_once_with("test_slug")
@pytest.mark.usefixtures("hassio_client", "setup_integration") @pytest.mark.usefixtures("hassio_client", "setup_integration")
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -653,6 +839,65 @@ async def test_reader_writer_create_wrong_parameters(
supervisor_client.backups.partial_backup.assert_not_called() supervisor_client.backups.partial_backup.assert_not_called()
@pytest.mark.usefixtures("hassio_client", "setup_integration")
async def test_agent_receive_remote_backup(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
supervisor_client: AsyncMock,
) -> None:
"""Test receiving a backup which will be uploaded to a remote agent."""
client = await hass_client()
backup_id = "test-backup"
supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS
supervisor_client.backups.upload_backup.return_value = "test_slug"
test_backup = AgentBackup(
addons=[AddonInfo(name="Test", slug="test", version="1.0.0")],
backup_id=backup_id,
database_included=True,
date="1970-01-01T00:00:00.000Z",
folders=[Folder.MEDIA, Folder.SHARE],
homeassistant_included=True,
homeassistant_version="2024.12.0",
name="Test",
protected=False,
size=0.0,
)
remote_agent = BackupAgentTest("remote")
await _setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
supervisor_client.backups.reload.assert_not_called()
with (
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.open"),
patch(
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
) as fetch_backup,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=test_backup,
),
patch("shutil.copy"),
):
fetch_backup.return_value = test_backup
resp = await client.post(
"/api/backup/upload?agent_id=test.remote",
data={"file": StringIO("test")},
)
assert resp.status == 201
supervisor_client.backups.download_backup.assert_called_once_with("test_slug")
supervisor_client.backups.remove_backup.assert_called_once_with("test_slug")
@pytest.mark.usefixtures("hassio_client", "setup_integration") @pytest.mark.usefixtures("hassio_client", "setup_integration")
async def test_reader_writer_restore( async def test_reader_writer_restore(
hass: HomeAssistant, hass: HomeAssistant,