"""Test the backups for WebDAV.""" from __future__ import annotations from collections.abc import AsyncGenerator from io import StringIO from unittest.mock import Mock, patch from aiowebdav2 import Property from aiowebdav2.exceptions import UnauthorizedError, WebDavError import pytest from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup from homeassistant.components.webdav.backup import async_register_backup_agents_listener from homeassistant.components.webdav.const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers.backup import async_initialize_backup from homeassistant.setup import async_setup_component from .const import BACKUP_METADATA, MOCK_LIST_WITH_PROPERTIES from tests.common import AsyncMock, MockConfigEntry from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture(autouse=True) async def setup_backup_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, webdav_client: AsyncMock ) -> AsyncGenerator[None]: """Set up webdav integration.""" with ( patch("homeassistant.components.backup.is_hassio", return_value=False), patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0), ): async_initialize_backup(hass) assert await async_setup_component(hass, BACKUP_DOMAIN, {}) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() yield async def test_agents_info( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_config_entry: MockConfigEntry, ) -> None: """Test backup agent info.""" client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "backup/agents/info"}) response = await client.receive_json() assert response["success"] assert response["result"] == { "agents": [ {"agent_id": "backup.local", "name": "local"}, { "agent_id": f"{DOMAIN}.{mock_config_entry.entry_id}", "name": mock_config_entry.title, }, ], } async def test_agents_list_backups( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_config_entry: MockConfigEntry, ) -> None: """Test agent list backups.""" client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "backup/info"}) response = await client.receive_json() assert response["success"] assert response["result"]["agent_errors"] == {} assert response["result"]["backups"] == [ { "addons": [], "agents": { "webdav.01JKXV07ASC62D620DGYNG2R8H": { "protected": False, "size": 34519040, } }, "backup_id": "23e64aec", "date": "2025-02-10T17:47:22.727189+01:00", "database_included": True, "extra_metadata": {}, "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.2.1", "name": "Automatic backup 2025.2.1", "failed_agent_ids": [], "with_automatic_settings": None, } ] async def test_agents_get_backup( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_config_entry: MockConfigEntry, ) -> None: """Test agent get backup.""" backup_id = BACKUP_METADATA["backup_id"] client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) response = await client.receive_json() assert response["success"] assert response["result"]["agent_errors"] == {} assert response["result"]["backup"] == { "addons": [], "agents": { f"{DOMAIN}.{mock_config_entry.entry_id}": { "protected": False, "size": 34519040, } }, "backup_id": "23e64aec", "date": "2025-02-10T17:47:22.727189+01:00", "database_included": True, "extra_metadata": {}, "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.2.1", "name": "Automatic backup 2025.2.1", "failed_agent_ids": [], "with_automatic_settings": None, } async def test_agents_delete( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, webdav_client: AsyncMock, ) -> None: """Test agent delete backup.""" client = await hass_ws_client(hass) await client.send_json_auto_id( { "type": "backup/delete", "backup_id": BACKUP_METADATA["backup_id"], } ) response = await client.receive_json() assert response["success"] assert response["result"] == {"agent_errors": {}} assert webdav_client.clean.call_count == 2 async def test_agents_upload( hass_client: ClientSessionGenerator, webdav_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test agent upload backup.""" client = await hass_client() test_backup = AgentBackup.from_dict(BACKUP_METADATA) with ( patch( "homeassistant.components.backup.manager.BackupManager.async_get_backup", ) as fetch_backup, patch( "homeassistant.components.backup.manager.read_backup", return_value=test_backup, ), patch("pathlib.Path.open") as mocked_open, ): mocked_open.return_value.read = Mock(side_effect=[b"test", b""]) fetch_backup.return_value = test_backup resp = await client.post( f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.entry_id}", data={"file": StringIO("test")}, ) assert resp.status == 201 assert webdav_client.upload_iter.call_count == 2 assert webdav_client.set_property_batch.call_count == 1 async def test_agents_download( hass_client: ClientSessionGenerator, webdav_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test agent download backup.""" client = await hass_client() backup_id = BACKUP_METADATA["backup_id"] resp = await client.get( f"/api/backup/download/{backup_id}?agent_id={DOMAIN}.{mock_config_entry.entry_id}" ) assert resp.status == 200 assert await resp.content.read() == b"backup data" async def test_error_on_agents_download( hass_client: ClientSessionGenerator, webdav_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test we get not found on a not existing backup on download.""" client = await hass_client() backup_id = BACKUP_METADATA["backup_id"] webdav_client.list_with_properties.side_effect = [MOCK_LIST_WITH_PROPERTIES, {}] resp = await client.get( f"/api/backup/download/{backup_id}?agent_id={DOMAIN}.{mock_config_entry.entry_id}" ) assert resp.status == 404 @pytest.mark.parametrize( ("side_effect", "error"), [ ( WebDavError("Unknown path"), "Backup operation failed: Unknown path", ), (TimeoutError(), "Backup operation timed out"), ], ) async def test_delete_error( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, webdav_client: AsyncMock, mock_config_entry: MockConfigEntry, side_effect: Exception, error: str, ) -> None: """Test error during delete.""" webdav_client.clean.side_effect = side_effect client = await hass_ws_client(hass) await client.send_json_auto_id( { "type": "backup/delete", "backup_id": BACKUP_METADATA["backup_id"], } ) response = await client.receive_json() assert response["success"] assert response["result"] == { "agent_errors": {f"{DOMAIN}.{mock_config_entry.entry_id}": error} } async def test_agents_delete_not_found_does_not_throw( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, webdav_client: AsyncMock, ) -> None: """Test agent delete backup.""" webdav_client.list_with_properties.return_value = {} client = await hass_ws_client(hass) await client.send_json_auto_id( { "type": "backup/delete", "backup_id": BACKUP_METADATA["backup_id"], } ) response = await client.receive_json() assert response["success"] assert response["result"] == {"agent_errors": {}} async def test_agents_backup_not_found( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, webdav_client: AsyncMock, ) -> None: """Test backup not found.""" webdav_client.list_with_properties.return_value = [] backup_id = BACKUP_METADATA["backup_id"] client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) response = await client.receive_json() assert response["success"] assert response["result"]["backup"] is None async def test_raises_on_403( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, webdav_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test we raise on 403.""" webdav_client.list_with_properties.side_effect = UnauthorizedError( "https://webdav.example.com" ) backup_id = BACKUP_METADATA["backup_id"] client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) response = await client.receive_json() assert response["success"] assert response["result"]["agent_errors"] == { f"{DOMAIN}.{mock_config_entry.entry_id}": "Authentication error" } async def test_listeners_get_cleaned_up(hass: HomeAssistant) -> None: """Test listener gets cleaned up.""" listener = AsyncMock() remove_listener = async_register_backup_agents_listener(hass, listener=listener) # make sure it's the last listener hass.data[DATA_BACKUP_AGENT_LISTENERS] = [listener] remove_listener() assert hass.data.get(DATA_BACKUP_AGENT_LISTENERS) is None async def test_metadata_misses_backup_id( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, webdav_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test getting a backup when metadata has backup id property.""" MOCK_LIST_WITH_PROPERTIES[ "/Automatic_backup_2025.2.1_2025-02-10_18.31_30202686.metadata.json" ] = [ Property( namespace="homeassistant", name="metadata_version", value="1", ) ] webdav_client.list_with_properties.return_value = MOCK_LIST_WITH_PROPERTIES backup_id = BACKUP_METADATA["backup_id"] client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) response = await client.receive_json() assert response["success"] assert response["result"]["backup"] is None