Move backup backup onboarding API to an onboarding platform (#142713)
* Move backup backup onboarding API to an onboarding platform * Move additional test from onboarding to backup * Remove backup tests from onboardingpull/142785/head
parent
ad3c4d24b8
commit
234c4c1958
|
@ -0,0 +1,143 @@
|
|||
"""Backup onboarding views."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import KEY_HASS
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.onboarding import (
|
||||
BaseOnboardingView,
|
||||
NoAuthBaseOnboardingView,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
|
||||
|
||||
from . import BackupManager, Folder, IncorrectPasswordError, http as backup_http
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.onboarding import OnboardingStoreData
|
||||
|
||||
|
||||
async def async_setup_views(hass: HomeAssistant, data: OnboardingStoreData) -> None:
|
||||
"""Set up the backup views."""
|
||||
|
||||
hass.http.register_view(BackupInfoView(data))
|
||||
hass.http.register_view(RestoreBackupView(data))
|
||||
hass.http.register_view(UploadBackupView(data))
|
||||
|
||||
|
||||
def with_backup_manager[_ViewT: BaseOnboardingView, **_P](
|
||||
func: Callable[
|
||||
Concatenate[_ViewT, BackupManager, web.Request, _P],
|
||||
Coroutine[Any, Any, web.Response],
|
||||
],
|
||||
) -> Callable[Concatenate[_ViewT, web.Request, _P], Coroutine[Any, Any, web.Response]]:
|
||||
"""Home Assistant API decorator to check onboarding and inject manager."""
|
||||
|
||||
@wraps(func)
|
||||
async def with_backup(
|
||||
self: _ViewT,
|
||||
request: web.Request,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> web.Response:
|
||||
"""Check admin and call function."""
|
||||
if self._data["done"]:
|
||||
raise HTTPUnauthorized
|
||||
|
||||
try:
|
||||
manager = await async_get_backup_manager(request.app[KEY_HASS])
|
||||
except HomeAssistantError:
|
||||
return self.json(
|
||||
{"code": "backup_disabled"},
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
return await func(self, manager, request, *args, **kwargs)
|
||||
|
||||
return with_backup
|
||||
|
||||
|
||||
class BackupInfoView(NoAuthBaseOnboardingView):
|
||||
"""Get backup info view."""
|
||||
|
||||
url = "/api/onboarding/backup/info"
|
||||
name = "api:onboarding:backup:info"
|
||||
|
||||
@with_backup_manager
|
||||
async def get(self, manager: BackupManager, request: web.Request) -> web.Response:
|
||||
"""Return backup info."""
|
||||
backups, _ = await manager.async_get_backups()
|
||||
return self.json(
|
||||
{
|
||||
"backups": list(backups.values()),
|
||||
"state": manager.state,
|
||||
"last_action_event": manager.last_action_event,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RestoreBackupView(NoAuthBaseOnboardingView):
|
||||
"""Restore backup view."""
|
||||
|
||||
url = "/api/onboarding/backup/restore"
|
||||
name = "api:onboarding:backup:restore"
|
||||
|
||||
@RequestDataValidator(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("backup_id"): str,
|
||||
vol.Required("agent_id"): str,
|
||||
vol.Optional("password"): str,
|
||||
vol.Optional("restore_addons"): [str],
|
||||
vol.Optional("restore_database", default=True): bool,
|
||||
vol.Optional("restore_folders"): [vol.Coerce(Folder)],
|
||||
}
|
||||
)
|
||||
)
|
||||
@with_backup_manager
|
||||
async def post(
|
||||
self, manager: BackupManager, request: web.Request, data: dict[str, Any]
|
||||
) -> web.Response:
|
||||
"""Restore a backup."""
|
||||
try:
|
||||
await manager.async_restore_backup(
|
||||
data["backup_id"],
|
||||
agent_id=data["agent_id"],
|
||||
password=data.get("password"),
|
||||
restore_addons=data.get("restore_addons"),
|
||||
restore_database=data["restore_database"],
|
||||
restore_folders=data.get("restore_folders"),
|
||||
restore_homeassistant=True,
|
||||
)
|
||||
except IncorrectPasswordError:
|
||||
return self.json(
|
||||
{"code": "incorrect_password"}, status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
return self.json(
|
||||
{"code": "restore_failed", "message": str(err)},
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
return web.Response(status=HTTPStatus.OK)
|
||||
|
||||
|
||||
class UploadBackupView(NoAuthBaseOnboardingView, backup_http.UploadBackupView):
|
||||
"""Upload backup view."""
|
||||
|
||||
url = "/api/onboarding/backup/upload"
|
||||
name = "api:onboarding:backup:upload"
|
||||
|
||||
@with_backup_manager
|
||||
async def post(self, manager: BackupManager, request: web.Request) -> web.Response:
|
||||
"""Upload a backup file."""
|
||||
return await self._post(request)
|
|
@ -3,11 +3,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, Protocol, cast
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||
|
@ -17,19 +15,11 @@ from homeassistant.auth.const import GROUP_ID_ADMIN
|
|||
from homeassistant.auth.providers.homeassistant import HassAuthProvider
|
||||
from homeassistant.components import person
|
||||
from homeassistant.components.auth import indieauth
|
||||
from homeassistant.components.backup import (
|
||||
BackupManager,
|
||||
Folder,
|
||||
IncorrectPasswordError,
|
||||
http as backup_http,
|
||||
)
|
||||
from homeassistant.components.http import KEY_HASS, KEY_HASS_REFRESH_TOKEN_ID
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import area_registry as ar, integration_platform
|
||||
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.setup import async_setup_component, async_wait_component
|
||||
|
@ -61,9 +51,6 @@ async def async_setup(
|
|||
hass.http.register_view(CoreConfigOnboardingView(data, store))
|
||||
hass.http.register_view(IntegrationOnboardingView(data, store))
|
||||
hass.http.register_view(AnalyticsOnboardingView(data, store))
|
||||
hass.http.register_view(BackupInfoView(data))
|
||||
hass.http.register_view(RestoreBackupView(data))
|
||||
hass.http.register_view(UploadBackupView(data))
|
||||
hass.http.register_view(WaitIntegrationOnboardingView(data))
|
||||
|
||||
|
||||
|
@ -377,114 +364,6 @@ class AnalyticsOnboardingView(_BaseOnboardingStepView):
|
|||
return self.json({})
|
||||
|
||||
|
||||
def with_backup_manager[_ViewT: BaseOnboardingView, **_P](
|
||||
func: Callable[
|
||||
Concatenate[_ViewT, BackupManager, web.Request, _P],
|
||||
Coroutine[Any, Any, web.Response],
|
||||
],
|
||||
) -> Callable[Concatenate[_ViewT, web.Request, _P], Coroutine[Any, Any, web.Response]]:
|
||||
"""Home Assistant API decorator to check onboarding and inject manager."""
|
||||
|
||||
@wraps(func)
|
||||
async def with_backup(
|
||||
self: _ViewT,
|
||||
request: web.Request,
|
||||
*args: _P.args,
|
||||
**kwargs: _P.kwargs,
|
||||
) -> web.Response:
|
||||
"""Check admin and call function."""
|
||||
if self._data["done"]:
|
||||
raise HTTPUnauthorized
|
||||
|
||||
try:
|
||||
manager = await async_get_backup_manager(request.app[KEY_HASS])
|
||||
except HomeAssistantError:
|
||||
return self.json(
|
||||
{"code": "backup_disabled"},
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
return await func(self, manager, request, *args, **kwargs)
|
||||
|
||||
return with_backup
|
||||
|
||||
|
||||
class BackupInfoView(NoAuthBaseOnboardingView):
|
||||
"""Get backup info view."""
|
||||
|
||||
url = "/api/onboarding/backup/info"
|
||||
name = "api:onboarding:backup:info"
|
||||
|
||||
@with_backup_manager
|
||||
async def get(self, manager: BackupManager, request: web.Request) -> web.Response:
|
||||
"""Return backup info."""
|
||||
backups, _ = await manager.async_get_backups()
|
||||
return self.json(
|
||||
{
|
||||
"backups": list(backups.values()),
|
||||
"state": manager.state,
|
||||
"last_action_event": manager.last_action_event,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RestoreBackupView(NoAuthBaseOnboardingView):
|
||||
"""Restore backup view."""
|
||||
|
||||
url = "/api/onboarding/backup/restore"
|
||||
name = "api:onboarding:backup:restore"
|
||||
|
||||
@RequestDataValidator(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("backup_id"): str,
|
||||
vol.Required("agent_id"): str,
|
||||
vol.Optional("password"): str,
|
||||
vol.Optional("restore_addons"): [str],
|
||||
vol.Optional("restore_database", default=True): bool,
|
||||
vol.Optional("restore_folders"): [vol.Coerce(Folder)],
|
||||
}
|
||||
)
|
||||
)
|
||||
@with_backup_manager
|
||||
async def post(
|
||||
self, manager: BackupManager, request: web.Request, data: dict[str, Any]
|
||||
) -> web.Response:
|
||||
"""Restore a backup."""
|
||||
try:
|
||||
await manager.async_restore_backup(
|
||||
data["backup_id"],
|
||||
agent_id=data["agent_id"],
|
||||
password=data.get("password"),
|
||||
restore_addons=data.get("restore_addons"),
|
||||
restore_database=data["restore_database"],
|
||||
restore_folders=data.get("restore_folders"),
|
||||
restore_homeassistant=True,
|
||||
)
|
||||
except IncorrectPasswordError:
|
||||
return self.json(
|
||||
{"code": "incorrect_password"}, status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
return self.json(
|
||||
{"code": "restore_failed", "message": str(err)},
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
return web.Response(status=HTTPStatus.OK)
|
||||
|
||||
|
||||
class UploadBackupView(NoAuthBaseOnboardingView, backup_http.UploadBackupView):
|
||||
"""Upload backup view."""
|
||||
|
||||
url = "/api/onboarding/backup/upload"
|
||||
name = "api:onboarding:backup:upload"
|
||||
|
||||
@with_backup_manager
|
||||
async def post(self, manager: BackupManager, request: web.Request) -> web.Response:
|
||||
"""Upload a backup file."""
|
||||
return await self._post(request)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_hass_provider(hass: HomeAssistant) -> HassAuthProvider:
|
||||
"""Get the Home Assistant auth provider."""
|
||||
|
|
|
@ -173,10 +173,6 @@ IGNORE_VIOLATIONS = {
|
|||
"logbook",
|
||||
# Temporary needed for migration until 2024.10
|
||||
("conversation", "assist_pipeline"),
|
||||
# The onboarding integration provides limited backup for use
|
||||
# during onboarding. The onboarding integration waits for the backup manager
|
||||
# and to be ready before calling any backup functionality.
|
||||
("onboarding", "backup"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,414 @@
|
|||
"""Test the onboarding views."""
|
||||
|
||||
from io import StringIO
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import backup, onboarding
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.backup import async_initialize_backup
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import register_auth_provider
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
def mock_onboarding_storage(hass_storage, data):
|
||||
"""Mock the onboarding storage."""
|
||||
hass_storage[onboarding.STORAGE_KEY] = {
|
||||
"version": onboarding.STORAGE_VERSION,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def auth_active(hass: HomeAssistant) -> None:
|
||||
"""Ensure auth is always active."""
|
||||
hass.loop.run_until_complete(
|
||||
register_auth_provider(hass, {"type": "homeassistant"})
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "view", "kwargs"),
|
||||
[
|
||||
("get", "backup/info", {}),
|
||||
(
|
||||
"post",
|
||||
"backup/restore",
|
||||
{"json": {"backup_id": "abc123", "agent_id": "test"}},
|
||||
),
|
||||
("post", "backup/upload", {}),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_view_after_done(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
method: str,
|
||||
view: str,
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test raising after onboarding."""
|
||||
mock_onboarding_storage(hass_storage, {"done": [onboarding.const.STEP_USER]})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.request(method, f"/api/onboarding/{view}", **kwargs)
|
||||
|
||||
assert resp.status == 401
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "view", "kwargs"),
|
||||
[
|
||||
("get", "backup/info", {}),
|
||||
(
|
||||
"post",
|
||||
"backup/restore",
|
||||
{"json": {"backup_id": "abc123", "agent_id": "test"}},
|
||||
),
|
||||
("post", "backup/upload", {}),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_view_without_backup(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
method: str,
|
||||
view: str,
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test interacting with backup wievs when backup integration is missing."""
|
||||
mock_onboarding_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.request(method, f"/api/onboarding/{view}", **kwargs)
|
||||
|
||||
assert resp.status == 404
|
||||
|
||||
|
||||
async def test_onboarding_backup_info(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test backup info."""
|
||||
mock_onboarding_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
backups = {
|
||||
"abc123": backup.ManagerBackup(
|
||||
addons=[backup.AddonInfo(name="Test", slug="test", version="1.0.0")],
|
||||
agents={
|
||||
"backup.local": backup.manager.AgentBackupStatus(protected=True, size=0)
|
||||
},
|
||||
backup_id="abc123",
|
||||
date="1970-01-01T00:00:00.000Z",
|
||||
database_included=True,
|
||||
extra_metadata={"instance_id": "abc123", "with_automatic_settings": True},
|
||||
folders=[backup.Folder.MEDIA, backup.Folder.SHARE],
|
||||
homeassistant_included=True,
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
failed_agent_ids=[],
|
||||
with_automatic_settings=True,
|
||||
),
|
||||
"def456": backup.ManagerBackup(
|
||||
addons=[],
|
||||
agents={
|
||||
"test.remote": backup.manager.AgentBackupStatus(protected=True, size=0)
|
||||
},
|
||||
backup_id="def456",
|
||||
date="1980-01-01T00:00:00.000Z",
|
||||
database_included=False,
|
||||
extra_metadata={
|
||||
"instance_id": "unknown_uuid",
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
folders=[backup.Folder.MEDIA, backup.Folder.SHARE],
|
||||
homeassistant_included=True,
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test 2",
|
||||
failed_agent_ids=[],
|
||||
with_automatic_settings=None,
|
||||
),
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backups",
|
||||
return_value=(backups, {}),
|
||||
):
|
||||
resp = await client.get("/api/onboarding/backup/info")
|
||||
|
||||
assert resp.status == 200
|
||||
assert await resp.json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("params", "expected_kwargs"),
|
||||
[
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
{
|
||||
"agent_id": "backup.local",
|
||||
"password": None,
|
||||
"restore_addons": None,
|
||||
"restore_database": True,
|
||||
"restore_folders": None,
|
||||
"restore_homeassistant": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1"],
|
||||
"restore_database": True,
|
||||
"restore_folders": ["media"],
|
||||
},
|
||||
{
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1"],
|
||||
"restore_database": True,
|
||||
"restore_folders": [backup.Folder.MEDIA],
|
||||
"restore_homeassistant": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1", "addon_2"],
|
||||
"restore_database": False,
|
||||
"restore_folders": ["media", "share"],
|
||||
},
|
||||
{
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1", "addon_2"],
|
||||
"restore_database": False,
|
||||
"restore_folders": [backup.Folder.MEDIA, backup.Folder.SHARE],
|
||||
"restore_homeassistant": True,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_restore(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
params: dict[str, Any],
|
||||
expected_kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test restore backup."""
|
||||
mock_onboarding_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_restore_backup",
|
||||
) as mock_restore:
|
||||
resp = await client.post("/api/onboarding/backup/restore", json=params)
|
||||
assert resp.status == 200
|
||||
mock_restore.assert_called_once_with("abc123", **expected_kwargs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("params", "restore_error", "expected_status", "expected_json", "restore_calls"),
|
||||
[
|
||||
# Missing agent_id
|
||||
(
|
||||
{"backup_id": "abc123"},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: required key not provided @ data['agent_id']"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Missing backup_id
|
||||
(
|
||||
{"agent_id": "backup.local"},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: required key not provided @ data['backup_id']"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Invalid restore_database
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"restore_database": "yes_please",
|
||||
},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: expected bool for dictionary value @ data['restore_database']"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Invalid folder
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"restore_folders": ["invalid"],
|
||||
},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: expected Folder or one of 'share', 'addons/local', 'ssl', 'media' @ data['restore_folders'][0]"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Wrong password
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
backup.IncorrectPasswordError,
|
||||
400,
|
||||
{"code": "incorrect_password"},
|
||||
1,
|
||||
),
|
||||
# Home Assistant error
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
HomeAssistantError("Boom!"),
|
||||
400,
|
||||
{"code": "restore_failed", "message": "Boom!"},
|
||||
1,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_restore_error(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
params: dict[str, Any],
|
||||
restore_error: Exception | None,
|
||||
expected_status: int,
|
||||
expected_json: str,
|
||||
restore_calls: int,
|
||||
) -> None:
|
||||
"""Test restore backup fails."""
|
||||
mock_onboarding_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_restore_backup",
|
||||
side_effect=restore_error,
|
||||
) as mock_restore:
|
||||
resp = await client.post("/api/onboarding/backup/restore", json=params)
|
||||
|
||||
assert resp.status == expected_status
|
||||
assert await resp.json() == expected_json
|
||||
assert len(mock_restore.mock_calls) == restore_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("params", "restore_error", "expected_status", "expected_message", "restore_calls"),
|
||||
[
|
||||
# Unexpected error
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
Exception("Boom!"),
|
||||
500,
|
||||
"500 Internal Server Error",
|
||||
1,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_restore_unexpected_error(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
params: dict[str, Any],
|
||||
restore_error: Exception | None,
|
||||
expected_status: int,
|
||||
expected_message: str,
|
||||
restore_calls: int,
|
||||
) -> None:
|
||||
"""Test restore backup fails."""
|
||||
mock_onboarding_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_restore_backup",
|
||||
side_effect=restore_error,
|
||||
) as mock_restore:
|
||||
resp = await client.post("/api/onboarding/backup/restore", json=params)
|
||||
|
||||
assert resp.status == expected_status
|
||||
assert (await resp.content.read()).decode().startswith(expected_message)
|
||||
assert len(mock_restore.mock_calls) == restore_calls
|
||||
|
||||
|
||||
async def test_onboarding_backup_upload(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test upload backup."""
|
||||
mock_onboarding_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_receive_backup",
|
||||
return_value="abc123",
|
||||
) as mock_receive:
|
||||
resp = await client.post(
|
||||
"/api/onboarding/backup/upload?agent_id=backup.local",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
assert resp.status == 201
|
||||
assert await resp.json() == {"backup_id": "abc123"}
|
||||
mock_receive.assert_called_once_with(agent_ids=["backup.local"], contents=ANY)
|
|
@ -3,20 +3,16 @@
|
|||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from http import HTTPStatus
|
||||
from io import StringIO
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import backup, onboarding
|
||||
from homeassistant.components import onboarding
|
||||
from homeassistant.components.onboarding import const, views
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import area_registry as ar
|
||||
from homeassistant.helpers.backup import async_initialize_backup
|
||||
from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component
|
||||
|
||||
from . import mock_storage
|
||||
|
@ -632,13 +628,6 @@ async def test_onboarding_installation_type(
|
|||
("method", "view", "kwargs"),
|
||||
[
|
||||
("get", "installation_type", {}),
|
||||
("get", "backup/info", {}),
|
||||
(
|
||||
"post",
|
||||
"backup/restore",
|
||||
{"json": {"backup_id": "abc123", "agent_id": "test"}},
|
||||
),
|
||||
("post", "backup/upload", {}),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_view_after_done(
|
||||
|
@ -723,353 +712,6 @@ async def test_complete_onboarding(
|
|||
listener_3.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "view", "kwargs"),
|
||||
[
|
||||
("get", "backup/info", {}),
|
||||
(
|
||||
"post",
|
||||
"backup/restore",
|
||||
{"json": {"backup_id": "abc123", "agent_id": "test"}},
|
||||
),
|
||||
("post", "backup/upload", {}),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_view_without_backup(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
method: str,
|
||||
view: str,
|
||||
kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test interacting with backup wievs when backup integration is missing."""
|
||||
mock_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.request(method, f"/api/onboarding/{view}", **kwargs)
|
||||
|
||||
assert resp.status == 500
|
||||
assert await resp.json() == {"code": "backup_disabled"}
|
||||
|
||||
|
||||
async def test_onboarding_backup_info(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test backup info."""
|
||||
mock_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
backups = {
|
||||
"abc123": backup.ManagerBackup(
|
||||
addons=[backup.AddonInfo(name="Test", slug="test", version="1.0.0")],
|
||||
agents={
|
||||
"backup.local": backup.manager.AgentBackupStatus(protected=True, size=0)
|
||||
},
|
||||
backup_id="abc123",
|
||||
date="1970-01-01T00:00:00.000Z",
|
||||
database_included=True,
|
||||
extra_metadata={"instance_id": "abc123", "with_automatic_settings": True},
|
||||
folders=[backup.Folder.MEDIA, backup.Folder.SHARE],
|
||||
homeassistant_included=True,
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
failed_agent_ids=[],
|
||||
with_automatic_settings=True,
|
||||
),
|
||||
"def456": backup.ManagerBackup(
|
||||
addons=[],
|
||||
agents={
|
||||
"test.remote": backup.manager.AgentBackupStatus(protected=True, size=0)
|
||||
},
|
||||
backup_id="def456",
|
||||
date="1980-01-01T00:00:00.000Z",
|
||||
database_included=False,
|
||||
extra_metadata={
|
||||
"instance_id": "unknown_uuid",
|
||||
"with_automatic_settings": True,
|
||||
},
|
||||
folders=[backup.Folder.MEDIA, backup.Folder.SHARE],
|
||||
homeassistant_included=True,
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test 2",
|
||||
failed_agent_ids=[],
|
||||
with_automatic_settings=None,
|
||||
),
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_get_backups",
|
||||
return_value=(backups, {}),
|
||||
):
|
||||
resp = await client.get("/api/onboarding/backup/info")
|
||||
|
||||
assert resp.status == 200
|
||||
assert await resp.json() == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("params", "expected_kwargs"),
|
||||
[
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
{
|
||||
"agent_id": "backup.local",
|
||||
"password": None,
|
||||
"restore_addons": None,
|
||||
"restore_database": True,
|
||||
"restore_folders": None,
|
||||
"restore_homeassistant": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1"],
|
||||
"restore_database": True,
|
||||
"restore_folders": ["media"],
|
||||
},
|
||||
{
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1"],
|
||||
"restore_database": True,
|
||||
"restore_folders": [backup.Folder.MEDIA],
|
||||
"restore_homeassistant": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1", "addon_2"],
|
||||
"restore_database": False,
|
||||
"restore_folders": ["media", "share"],
|
||||
},
|
||||
{
|
||||
"agent_id": "backup.local",
|
||||
"password": "hunter2",
|
||||
"restore_addons": ["addon_1", "addon_2"],
|
||||
"restore_database": False,
|
||||
"restore_folders": [backup.Folder.MEDIA, backup.Folder.SHARE],
|
||||
"restore_homeassistant": True,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_restore(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
params: dict[str, Any],
|
||||
expected_kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test restore backup."""
|
||||
mock_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_restore_backup",
|
||||
) as mock_restore:
|
||||
resp = await client.post("/api/onboarding/backup/restore", json=params)
|
||||
assert resp.status == 200
|
||||
mock_restore.assert_called_once_with("abc123", **expected_kwargs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("params", "restore_error", "expected_status", "expected_json", "restore_calls"),
|
||||
[
|
||||
# Missing agent_id
|
||||
(
|
||||
{"backup_id": "abc123"},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: required key not provided @ data['agent_id']"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Missing backup_id
|
||||
(
|
||||
{"agent_id": "backup.local"},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: required key not provided @ data['backup_id']"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Invalid restore_database
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"restore_database": "yes_please",
|
||||
},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: expected bool for dictionary value @ data['restore_database']"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Invalid folder
|
||||
(
|
||||
{
|
||||
"backup_id": "abc123",
|
||||
"agent_id": "backup.local",
|
||||
"restore_folders": ["invalid"],
|
||||
},
|
||||
None,
|
||||
400,
|
||||
{
|
||||
"message": "Message format incorrect: expected Folder or one of 'share', 'addons/local', 'ssl', 'media' @ data['restore_folders'][0]"
|
||||
},
|
||||
0,
|
||||
),
|
||||
# Wrong password
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
backup.IncorrectPasswordError,
|
||||
400,
|
||||
{"code": "incorrect_password"},
|
||||
1,
|
||||
),
|
||||
# Home Assistant error
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
HomeAssistantError("Boom!"),
|
||||
400,
|
||||
{"code": "restore_failed", "message": "Boom!"},
|
||||
1,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_restore_error(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
params: dict[str, Any],
|
||||
restore_error: Exception | None,
|
||||
expected_status: int,
|
||||
expected_json: str,
|
||||
restore_calls: int,
|
||||
) -> None:
|
||||
"""Test restore backup fails."""
|
||||
mock_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_restore_backup",
|
||||
side_effect=restore_error,
|
||||
) as mock_restore:
|
||||
resp = await client.post("/api/onboarding/backup/restore", json=params)
|
||||
|
||||
assert resp.status == expected_status
|
||||
assert await resp.json() == expected_json
|
||||
assert len(mock_restore.mock_calls) == restore_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("params", "restore_error", "expected_status", "expected_message", "restore_calls"),
|
||||
[
|
||||
# Unexpected error
|
||||
(
|
||||
{"backup_id": "abc123", "agent_id": "backup.local"},
|
||||
Exception("Boom!"),
|
||||
500,
|
||||
"500 Internal Server Error",
|
||||
1,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_onboarding_backup_restore_unexpected_error(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
params: dict[str, Any],
|
||||
restore_error: Exception | None,
|
||||
expected_status: int,
|
||||
expected_message: str,
|
||||
restore_calls: int,
|
||||
) -> None:
|
||||
"""Test restore backup fails."""
|
||||
mock_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_restore_backup",
|
||||
side_effect=restore_error,
|
||||
) as mock_restore:
|
||||
resp = await client.post("/api/onboarding/backup/restore", json=params)
|
||||
|
||||
assert resp.status == expected_status
|
||||
assert (await resp.content.read()).decode().startswith(expected_message)
|
||||
assert len(mock_restore.mock_calls) == restore_calls
|
||||
|
||||
|
||||
async def test_onboarding_backup_upload(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test upload backup."""
|
||||
mock_storage(hass_storage, {"done": []})
|
||||
|
||||
assert await async_setup_component(hass, "onboarding", {})
|
||||
async_initialize_backup(hass)
|
||||
assert await async_setup_component(hass, "backup", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.backup.manager.BackupManager.async_receive_backup",
|
||||
return_value="abc123",
|
||||
) as mock_receive:
|
||||
resp = await client.post(
|
||||
"/api/onboarding/backup/upload?agent_id=backup.local",
|
||||
data={"file": StringIO("test")},
|
||||
)
|
||||
assert resp.status == 201
|
||||
assert await resp.json() == {"backup_id": "abc123"}
|
||||
mock_receive.assert_called_once_with(agent_ids=["backup.local"], contents=ANY)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "expected_result"),
|
||||
[
|
||||
|
|
Loading…
Reference in New Issue