Add receive backup tests (#135680)

* Clean up test_receive_backup_busy_manager

* Test receive backup agent error

* Test file write error during backup receive

* Test read tar error during backup receive

* Test non agent upload error during backup receive

* Test file read error during backup receive
This commit is contained in:
Martin Hjelmare 2025-01-16 08:41:59 +01:00 committed by GitHub
parent a8645ea4ed
commit 77a351f992
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -8,8 +8,18 @@ from dataclasses import replace
from io import StringIO from io import StringIO
import json import json
from pathlib import Path from pathlib import Path
import tarfile
from typing import Any from typing import Any
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, call, mock_open, patch from unittest.mock import (
ANY,
DEFAULT,
AsyncMock,
MagicMock,
Mock,
call,
mock_open,
patch,
)
import pytest import pytest
@ -33,6 +43,8 @@ from homeassistant.components.backup.manager import (
CreateBackupStage, CreateBackupStage,
CreateBackupState, CreateBackupState,
NewBackup, NewBackup,
ReceiveBackupStage,
ReceiveBackupState,
WrittenBackup, WrittenBackup,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -1429,8 +1441,12 @@ async def test_receive_backup_busy_manager(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
create_backup: AsyncMock,
) -> None: ) -> None:
"""Test receive backup with a busy manager.""" """Test receive backup with a busy manager."""
new_backup = NewBackup(backup_job_id="time-123")
backup_task: asyncio.Future[WrittenBackup] = asyncio.Future()
create_backup.return_value = (new_backup, backup_task)
assert await async_setup_component(hass, DOMAIN, {}) assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -1445,24 +1461,18 @@ async def test_receive_backup_busy_manager(
result = await ws_client.receive_json() result = await ws_client.receive_json()
assert result["success"] is True assert result["success"] is True
new_backup = NewBackup(backup_job_id="time-123") await ws_client.send_json_auto_id(
backup_task: asyncio.Future[WrittenBackup] = asyncio.Future() {"type": "backup/generate", "agent_ids": ["backup.local"]}
with patch( )
"homeassistant.components.backup.manager.CoreBackupReaderWriter.async_create_backup", result = await ws_client.receive_json()
return_value=(new_backup, backup_task), assert result["event"] == {
) as create_backup: "manager_state": "create_backup",
await ws_client.send_json_auto_id( "stage": None,
{"type": "backup/generate", "agent_ids": ["backup.local"]} "state": "in_progress",
) }
result = await ws_client.receive_json() result = await ws_client.receive_json()
assert result["event"] == { assert result["success"] is True
"manager_state": "create_backup", assert result["result"] == {"backup_job_id": "time-123"}
"stage": None,
"state": "in_progress",
}
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {"backup_job_id": "time-123"}
assert create_backup.call_count == 1 assert create_backup.call_count == 1
@ -1488,6 +1498,732 @@ async def test_receive_backup_busy_manager(
await hass.async_block_till_done() await hass.async_block_till_done()
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize("exception", [BackupAgentError("Boom!"), Exception("Boom!")])
async def test_receive_backup_agent_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
path_glob: MagicMock,
hass_storage: dict[str, Any],
exception: Exception,
) -> None:
"""Test upload error during backup receive."""
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
backup_1 = replace(TEST_BACKUP_ABC123, backup_id="backup1") # matching instance id
backup_2 = replace(TEST_BACKUP_DEF456, backup_id="backup2") # other instance id
backup_3 = replace(TEST_BACKUP_ABC123, backup_id="backup3") # matching instance id
backups_info: list[dict[str, Any]] = [
{
"addons": [
{
"name": "Test",
"slug": "test",
"version": "1.0.0",
},
],
"agent_ids": [
"test.remote",
],
"backup_id": "backup1",
"database_included": True,
"date": "1970-01-01T00:00:00.000Z",
"failed_agent_ids": [],
"folders": [
"media",
"share",
],
"homeassistant_included": True,
"homeassistant_version": "2024.12.0",
"name": "Test",
"protected": False,
"size": 0,
"with_automatic_settings": True,
},
{
"addons": [],
"agent_ids": [
"test.remote",
],
"backup_id": "backup2",
"database_included": False,
"date": "1980-01-01T00:00:00.000Z",
"failed_agent_ids": [],
"folders": [
"media",
"share",
],
"homeassistant_included": True,
"homeassistant_version": "2024.12.0",
"name": "Test 2",
"protected": False,
"size": 1,
"with_automatic_settings": None,
},
{
"addons": [
{
"name": "Test",
"slug": "test",
"version": "1.0.0",
},
],
"agent_ids": [
"test.remote",
],
"backup_id": "backup3",
"database_included": True,
"date": "1970-01-01T00:00:00.000Z",
"failed_agent_ids": [],
"folders": [
"media",
"share",
],
"homeassistant_included": True,
"homeassistant_version": "2024.12.0",
"name": "Test",
"protected": False,
"size": 0,
"with_automatic_settings": True,
},
]
remote_agent = BackupAgentTest("remote", backups=[backup_1, backup_2, backup_3])
with patch(
"homeassistant.components.backup.backup.async_get_backup_agents"
) as core_get_backup_agents:
core_get_backup_agents.return_value = [local_agent]
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
await setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
client = await hass_client()
ws_client = await hass_ws_client(hass)
path_glob.return_value = []
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"backups": backups_info,
"agent_errors": {},
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
}
await ws_client.send_json_auto_id(
{"type": "backup/config/update", "retention": {"copies": 1, "days": None}}
)
result = await ws_client.receive_json()
assert result["success"]
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
result = await ws_client.receive_json()
assert result["success"] is True
delete_backup = AsyncMock()
upload_data = "test"
open_mock = mock_open(read_data=upload_data.encode(encoding="utf-8"))
with (
patch.object(remote_agent, "async_delete_backup", delete_backup),
patch.object(remote_agent, "async_upload_backup", side_effect=exception),
patch("pathlib.Path.open", open_mock),
patch("shutil.move") as move_mock,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=TEST_BACKUP_ABC123,
),
patch("pathlib.Path.unlink") as unlink_mock,
):
resp = await client.post(
"/api/backup/upload?agent_id=test.remote",
data={"file": StringIO(upload_data)},
)
await hass.async_block_till_done()
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.RECEIVE_FILE,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.UPLOAD_TO_AGENTS,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.COMPLETED,
}
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"backups": backups_info,
"agent_errors": {},
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
}
await hass.async_block_till_done()
assert hass_storage[DOMAIN]["data"]["backups"] == [
{
"backup_id": "abc123",
"failed_agent_ids": ["test.remote"],
}
]
assert resp.status == 201
assert open_mock.call_count == 1
assert move_mock.call_count == 0
assert unlink_mock.call_count == 1
assert delete_backup.call_count == 0
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize("exception", [asyncio.CancelledError("Boom!")])
async def test_receive_backup_non_agent_upload_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
path_glob: MagicMock,
hass_storage: dict[str, Any],
exception: Exception,
) -> None:
"""Test non agent upload error during backup receive."""
hass_storage[DOMAIN] = {
"data": {},
"key": DOMAIN,
"version": 1,
}
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
remote_agent = BackupAgentTest("remote", backups=[])
with patch(
"homeassistant.components.backup.backup.async_get_backup_agents"
) as core_get_backup_agents:
core_get_backup_agents.return_value = [local_agent]
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
await setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
client = await hass_client()
ws_client = await hass_ws_client(hass)
path_glob.return_value = []
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"backups": [],
"agent_errors": {},
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
}
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
result = await ws_client.receive_json()
assert result["success"] is True
upload_data = "test"
open_mock = mock_open(read_data=upload_data.encode(encoding="utf-8"))
with (
patch.object(remote_agent, "async_upload_backup", side_effect=exception),
patch("pathlib.Path.open", open_mock),
patch("shutil.move") as move_mock,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=TEST_BACKUP_ABC123,
),
patch("pathlib.Path.unlink") as unlink_mock,
):
resp = await client.post(
"/api/backup/upload?agent_id=test.remote",
data={"file": StringIO(upload_data)},
)
await hass.async_block_till_done()
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.RECEIVE_FILE,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.UPLOAD_TO_AGENTS,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
assert not hass_storage[DOMAIN]["data"]
assert resp.status == 500
assert open_mock.call_count == 1
assert move_mock.call_count == 0
assert unlink_mock.call_count == 0
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize(
(
"open_call_count",
"open_exception",
"write_call_count",
"write_exception",
"close_call_count",
"close_exception",
),
[
(1, OSError("Boom!"), 0, None, 0, None),
(1, None, 1, OSError("Boom!"), 1, None),
(1, None, 1, None, 1, OSError("Boom!")),
],
)
async def test_receive_backup_file_write_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
path_glob: MagicMock,
open_call_count: int,
open_exception: Exception | None,
write_call_count: int,
write_exception: Exception | None,
close_call_count: int,
close_exception: Exception | None,
) -> None:
"""Test file write error during backup receive."""
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
remote_agent = BackupAgentTest("remote", backups=[])
with patch(
"homeassistant.components.backup.backup.async_get_backup_agents"
) as core_get_backup_agents:
core_get_backup_agents.return_value = [local_agent]
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
await setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
client = await hass_client()
ws_client = await hass_ws_client(hass)
path_glob.return_value = []
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"backups": [],
"agent_errors": {},
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
}
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
result = await ws_client.receive_json()
assert result["success"] is True
upload_data = "test"
open_mock = mock_open(read_data=upload_data.encode(encoding="utf-8"))
open_mock.side_effect = open_exception
open_mock.return_value.write.side_effect = write_exception
open_mock.return_value.close.side_effect = close_exception
with (
patch("pathlib.Path.open", open_mock),
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=TEST_BACKUP_ABC123,
),
):
resp = await client.post(
"/api/backup/upload?agent_id=test.remote",
data={"file": StringIO(upload_data)},
)
await hass.async_block_till_done()
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.RECEIVE_FILE,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.FAILED,
}
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
assert resp.status == 500
assert open_mock.call_count == open_call_count
assert open_mock.return_value.write.call_count == write_call_count
assert open_mock.return_value.close.call_count == close_call_count
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize(
"exception",
[
OSError("Boom!"),
tarfile.TarError("Boom!"),
json.JSONDecodeError("Boom!", "test", 1),
KeyError("Boom!"),
],
)
async def test_receive_backup_read_tar_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
path_glob: MagicMock,
exception: Exception,
) -> None:
"""Test read tar error during backup receive."""
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
remote_agent = BackupAgentTest("remote", backups=[])
with patch(
"homeassistant.components.backup.backup.async_get_backup_agents"
) as core_get_backup_agents:
core_get_backup_agents.return_value = [local_agent]
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
await setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
client = await hass_client()
ws_client = await hass_ws_client(hass)
path_glob.return_value = []
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"backups": [],
"agent_errors": {},
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
}
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
result = await ws_client.receive_json()
assert result["success"] is True
upload_data = "test"
open_mock = mock_open(read_data=upload_data.encode(encoding="utf-8"))
with (
patch("pathlib.Path.open", open_mock),
patch(
"homeassistant.components.backup.manager.read_backup",
side_effect=exception,
) as read_backup,
):
resp = await client.post(
"/api/backup/upload?agent_id=test.remote",
data={"file": StringIO(upload_data)},
)
await hass.async_block_till_done()
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.RECEIVE_FILE,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.FAILED,
}
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
assert resp.status == 500
assert read_backup.call_count == 1
@pytest.mark.usefixtures("mock_backup_generation")
@pytest.mark.parametrize(
(
"open_call_count",
"open_exception",
"read_call_count",
"read_exception",
"close_call_count",
"close_exception",
"unlink_call_count",
"unlink_exception",
"final_state",
"response_status",
),
[
(
2,
[DEFAULT, OSError("Boom!")],
0,
None,
1,
[DEFAULT, DEFAULT],
1,
None,
ReceiveBackupState.COMPLETED,
201,
),
(
2,
[DEFAULT, DEFAULT],
1,
OSError("Boom!"),
2,
[DEFAULT, DEFAULT],
1,
None,
ReceiveBackupState.COMPLETED,
201,
),
(
2,
[DEFAULT, DEFAULT],
1,
None,
2,
[DEFAULT, OSError("Boom!")],
1,
None,
ReceiveBackupState.COMPLETED,
201,
),
(
2,
[DEFAULT, DEFAULT],
1,
None,
2,
[DEFAULT, DEFAULT],
1,
OSError("Boom!"),
ReceiveBackupState.FAILED,
500,
),
],
)
async def test_receive_backup_file_read_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
path_glob: MagicMock,
open_call_count: int,
open_exception: list[Exception | None],
read_call_count: int,
read_exception: Exception | None,
close_call_count: int,
close_exception: list[Exception | None],
unlink_call_count: int,
unlink_exception: Exception | None,
final_state: ReceiveBackupState,
response_status: int,
) -> None:
"""Test file read error during backup receive."""
local_agent = local_backup_platform.CoreLocalBackupAgent(hass)
remote_agent = BackupAgentTest("remote", backups=[])
with patch(
"homeassistant.components.backup.backup.async_get_backup_agents"
) as core_get_backup_agents:
core_get_backup_agents.return_value = [local_agent]
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
await setup_backup_platform(
hass,
domain="test",
platform=Mock(
async_get_backup_agents=AsyncMock(return_value=[remote_agent]),
spec_set=BackupAgentPlatformProtocol,
),
)
client = await hass_client()
ws_client = await hass_ws_client(hass)
path_glob.return_value = []
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"backups": [],
"agent_errors": {},
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
}
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
result = await ws_client.receive_json()
assert result["success"] is True
upload_data = "test"
open_mock = mock_open(read_data=upload_data.encode(encoding="utf-8"))
open_mock.side_effect = open_exception
open_mock.return_value.read.side_effect = read_exception
open_mock.return_value.close.side_effect = close_exception
with (
patch("pathlib.Path.open", open_mock),
patch("pathlib.Path.unlink", side_effect=unlink_exception) as unlink_mock,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=TEST_BACKUP_ABC123,
),
):
resp = await client.post(
"/api/backup/upload?agent_id=test.remote",
data={"file": StringIO(upload_data)},
)
await hass.async_block_till_done()
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.RECEIVE_FILE,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": ReceiveBackupStage.UPLOAD_TO_AGENTS,
"state": ReceiveBackupState.IN_PROGRESS,
}
result = await ws_client.receive_json()
assert result["event"] == {
"manager_state": BackupManagerState.RECEIVE_BACKUP,
"stage": None,
"state": final_state,
}
result = await ws_client.receive_json()
assert result["event"] == {"manager_state": BackupManagerState.IDLE}
assert resp.status == response_status
assert open_mock.call_count == open_call_count
assert open_mock.return_value.read.call_count == read_call_count
assert open_mock.return_value.close.call_count == close_call_count
assert unlink_mock.call_count == unlink_call_count
@pytest.mark.parametrize( @pytest.mark.parametrize(
("agent_id", "password", "restore_database", "restore_homeassistant", "dir"), ("agent_id", "password", "restore_database", "restore_homeassistant", "dir"),
[ [