273 lines
8.8 KiB
Python
273 lines
8.8 KiB
Python
"""Tests for the Backup integration."""
|
|
|
|
import asyncio
|
|
from collections.abc import AsyncIterator, Iterable
|
|
from io import BytesIO, StringIO
|
|
import json
|
|
import tarfile
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
from aiohttp import web
|
|
import pytest
|
|
|
|
from homeassistant.components.backup import AddonInfo, AgentBackup, Folder
|
|
from homeassistant.components.backup.const import DATA_MANAGER, DOMAIN
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from .common import TEST_BACKUP_ABC123, BackupAgentTest, setup_backup_integration
|
|
|
|
from tests.common import MockUser, get_fixture_path
|
|
from tests.typing import ClientSessionGenerator
|
|
|
|
|
|
async def test_downloading_local_backup(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test downloading a local backup file."""
|
|
await setup_backup_integration(hass)
|
|
|
|
client = await hass_client()
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.backup.backup.CoreLocalBackupAgent.async_get_backup",
|
|
return_value=TEST_BACKUP_ABC123,
|
|
),
|
|
patch("pathlib.Path.exists", return_value=True),
|
|
patch(
|
|
"homeassistant.components.backup.http.FileResponse",
|
|
return_value=web.Response(text=""),
|
|
),
|
|
):
|
|
resp = await client.get("/api/backup/download/abc123?agent_id=backup.local")
|
|
assert resp.status == 200
|
|
|
|
|
|
async def test_downloading_remote_backup(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test downloading a remote backup."""
|
|
await setup_backup_integration(
|
|
hass, backups={"test.test": [TEST_BACKUP_ABC123]}, remote_agents=["test"]
|
|
)
|
|
|
|
client = await hass_client()
|
|
|
|
with (
|
|
patch.object(BackupAgentTest, "async_download_backup") as download_mock,
|
|
):
|
|
download_mock.return_value.__aiter__.return_value = iter((b"backup data",))
|
|
resp = await client.get("/api/backup/download/abc123?agent_id=test.test")
|
|
assert resp.status == 200
|
|
assert await resp.content.read() == b"backup data"
|
|
|
|
|
|
async def test_downloading_local_encrypted_backup_file_not_found(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test downloading a local backup file."""
|
|
await setup_backup_integration(hass)
|
|
client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.backup.backup.CoreLocalBackupAgent.async_get_backup",
|
|
return_value=TEST_BACKUP_ABC123,
|
|
):
|
|
resp = await client.get(
|
|
"/api/backup/download/abc123?agent_id=backup.local&password=blah"
|
|
)
|
|
assert resp.status == 404
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_backups")
|
|
async def test_downloading_local_encrypted_backup(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test downloading a local backup file."""
|
|
await setup_backup_integration(hass)
|
|
await _test_downloading_encrypted_backup(hass_client, "backup.local")
|
|
|
|
|
|
async def aiter_from_iter(iterable: Iterable) -> AsyncIterator:
|
|
"""Convert an iterable to an async iterator."""
|
|
for i in iterable:
|
|
yield i
|
|
|
|
|
|
@patch.object(BackupAgentTest, "async_download_backup")
|
|
async def test_downloading_remote_encrypted_backup(
|
|
download_mock,
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test downloading a local backup file."""
|
|
backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN)
|
|
await setup_backup_integration(hass)
|
|
hass.data[DATA_MANAGER].backup_agents["domain.test"] = BackupAgentTest(
|
|
"test",
|
|
[
|
|
AgentBackup(
|
|
addons=[AddonInfo(name="Test", slug="test", version="1.0.0")],
|
|
backup_id="c0cb53bd",
|
|
database_included=True,
|
|
date="1970-01-01T00:00:00Z",
|
|
extra_metadata={},
|
|
folders=[Folder.MEDIA, Folder.SHARE],
|
|
homeassistant_included=True,
|
|
homeassistant_version="2024.12.0",
|
|
name="Test",
|
|
protected=True,
|
|
size=13,
|
|
)
|
|
],
|
|
)
|
|
|
|
async def download_backup(backup_id: str, **kwargs: Any) -> AsyncIterator[bytes]:
|
|
return aiter_from_iter((backup_path.read_bytes(),))
|
|
|
|
download_mock.side_effect = download_backup
|
|
await _test_downloading_encrypted_backup(hass_client, "domain.test")
|
|
|
|
|
|
async def _test_downloading_encrypted_backup(
|
|
hass_client: ClientSessionGenerator,
|
|
agent_id: str,
|
|
) -> None:
|
|
"""Test downloading an encrypted backup file."""
|
|
# Try downloading without supplying a password
|
|
client = await hass_client()
|
|
resp = await client.get(f"/api/backup/download/c0cb53bd?agent_id={agent_id}")
|
|
assert resp.status == 200
|
|
backup = await resp.read()
|
|
# We expect a valid outer tar file, but the inner tar file is encrypted and
|
|
# can't be read
|
|
with tarfile.open(fileobj=BytesIO(backup), mode="r") as outer_tar:
|
|
enc_metadata = json.loads(outer_tar.extractfile("./backup.json").read())
|
|
assert enc_metadata["protected"] is True
|
|
with (
|
|
outer_tar.extractfile("core.tar.gz") as inner_tar_file,
|
|
pytest.raises(tarfile.ReadError, match="file could not be opened"),
|
|
):
|
|
# pylint: disable-next=consider-using-with
|
|
tarfile.open(fileobj=inner_tar_file, mode="r")
|
|
|
|
# Download with the wrong password
|
|
resp = await client.get(
|
|
f"/api/backup/download/c0cb53bd?agent_id={agent_id}&password=wrong"
|
|
)
|
|
assert resp.status == 200
|
|
backup = await resp.read()
|
|
# We expect a truncated outer tar file
|
|
with (
|
|
tarfile.open(fileobj=BytesIO(backup), mode="r") as outer_tar,
|
|
pytest.raises(tarfile.ReadError, match="unexpected end of data"),
|
|
):
|
|
outer_tar.getnames()
|
|
|
|
# Finally download with the correct password
|
|
resp = await client.get(
|
|
f"/api/backup/download/c0cb53bd?agent_id={agent_id}&password=hunter2"
|
|
)
|
|
assert resp.status == 200
|
|
backup = await resp.read()
|
|
# We expect a valid outer tar file, the inner tar file is decrypted and can be read
|
|
with (
|
|
tarfile.open(fileobj=BytesIO(backup), mode="r") as outer_tar,
|
|
):
|
|
dec_metadata = json.loads(outer_tar.extractfile("./backup.json").read())
|
|
assert dec_metadata == enc_metadata | {"protected": False}
|
|
with (
|
|
outer_tar.extractfile("core.tar.gz") as inner_tar_file,
|
|
tarfile.open(fileobj=inner_tar_file, mode="r") as inner_tar,
|
|
):
|
|
assert inner_tar.getnames() == [
|
|
".",
|
|
"README.md",
|
|
"test_symlink",
|
|
"test1",
|
|
"test1/script.sh",
|
|
]
|
|
|
|
|
|
async def test_downloading_backup_not_found(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test downloading a backup file that does not exist."""
|
|
await setup_backup_integration(hass)
|
|
|
|
client = await hass_client()
|
|
|
|
resp = await client.get("/api/backup/download/abc123?agent_id=backup.local")
|
|
assert resp.status == 404
|
|
|
|
|
|
async def test_downloading_as_non_admin(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
hass_admin_user: MockUser,
|
|
) -> None:
|
|
"""Test downloading a backup file when you are not an admin."""
|
|
hass_admin_user.groups = []
|
|
await setup_backup_integration(hass)
|
|
|
|
client = await hass_client()
|
|
|
|
resp = await client.get("/api/backup/download/abc123?agent_id=backup.local")
|
|
assert resp.status == 401
|
|
|
|
|
|
async def test_uploading_a_backup_file(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
) -> None:
|
|
"""Test uploading a backup file."""
|
|
await setup_backup_integration(hass)
|
|
|
|
client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.backup.manager.BackupManager.async_receive_backup",
|
|
) as async_receive_backup_mock:
|
|
resp = await client.post(
|
|
"/api/backup/upload?agent_id=backup.local",
|
|
data={"file": StringIO("test")},
|
|
)
|
|
assert resp.status == 201
|
|
assert async_receive_backup_mock.called
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("error", "message"),
|
|
[
|
|
(OSError("Boom!"), "Can't write backup file: Boom!"),
|
|
(asyncio.CancelledError("Boom!"), ""),
|
|
],
|
|
)
|
|
async def test_error_handling_uploading_a_backup_file(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
error: Exception,
|
|
message: str,
|
|
) -> None:
|
|
"""Test error handling when uploading a backup file."""
|
|
await setup_backup_integration(hass)
|
|
|
|
client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.backup.manager.BackupManager.async_receive_backup",
|
|
side_effect=error,
|
|
):
|
|
resp = await client.post(
|
|
"/api/backup/upload?agent_id=backup.local",
|
|
data={"file": StringIO("test")},
|
|
)
|
|
assert resp.status == 500
|
|
assert await resp.text() == message
|