diff --git a/tests/components/backup/conftest.py b/tests/components/backup/conftest.py index 86656d8d976..d2416f78f6a 100644 --- a/tests/components/backup/conftest.py +++ b/tests/components/backup/conftest.py @@ -5,6 +5,7 @@ from __future__ import annotations from asyncio import Future from collections.abc import Generator from pathlib import Path +import shutil from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -13,11 +14,31 @@ from homeassistant.components.backup import DOMAIN from homeassistant.components.backup.manager import NewBackup, WrittenBackup from homeassistant.core import HomeAssistant -from .common import TEST_BACKUP_PATH_ABC123, TEST_BACKUP_PATH_DEF456 - from tests.common import get_fixture_path +@pytest.fixture +def available_backups() -> list[Path]: + """Fixture to provide available backup files.""" + return [] + + +@pytest.fixture +def hass_config_dir(tmp_path: Path, available_backups: list[Path]) -> str: + """Fixture to create a temporary config directory, populated with test files.""" + shutil.copytree( + get_fixture_path("config_dir_contents", DOMAIN), + tmp_path, + symlinks=True, + dirs_exist_ok=True, + ) + for backup in available_backups: + (get_fixture_path("test_backups", DOMAIN) / backup).copy_into( + tmp_path / "backups" + ) + return tmp_path.as_posix() + + @pytest.fixture(name="instance_id", autouse=True) def instance_id_fixture(hass: HomeAssistant) -> Generator[None]: """Mock instance ID.""" @@ -38,74 +59,6 @@ def mocked_json_bytes_fixture() -> Generator[Mock]: yield mocked_json_bytes -@pytest.fixture(name="mocked_tarfile") -def mocked_tarfile_fixture() -> Generator[Mock]: - """Mock tarfile.""" - with patch( - "homeassistant.components.backup.manager.SecureTarFile" - ) as mocked_tarfile: - yield mocked_tarfile - - -@pytest.fixture(name="path_glob") -def path_glob_fixture(hass: HomeAssistant) -> Generator[MagicMock]: - """Mock path glob.""" - with patch( - "pathlib.Path.glob", - return_value=[ - Path(hass.config.path()) / "backups" / TEST_BACKUP_PATH_ABC123, - Path(hass.config.path()) / "backups" / TEST_BACKUP_PATH_DEF456, - ], - ) as path_glob: - yield path_glob - - -CONFIG_DIR = { - "tests/testing_config": [ - Path("test.txt"), - Path(".DS_Store"), - Path(".storage"), - Path("another_subdir"), - Path("backups"), - Path("tmp_backups"), - Path("tts"), - Path("home-assistant_v2.db"), - ], - "/backups": [ - Path("backups/backup.tar"), - Path("backups/not_backup"), - ], - "/another_subdir": [ - Path("another_subdir/.DS_Store"), - Path("another_subdir/backups"), - Path("another_subdir/tts"), - ], - "another_subdir/backups": [ - Path("another_subdir/backups/backup.tar"), - Path("another_subdir/backups/not_backup"), - ], - "another_subdir/tts": [ - Path("another_subdir/tts/voice.mp3"), - ], - "/tmp_backups": [ # noqa: S108 - Path("tmp_backups/forgotten_backup.tar"), - Path("tmp_backups/not_backup"), - ], - "/tts": [ - Path("tts/voice.mp3"), - ], -} -CONFIG_DIR_DIRS = { - Path(".storage"), - Path("another_subdir"), - Path("another_subdir/backups"), - Path("another_subdir/tts"), - Path("backups"), - Path("tmp_backups"), - Path("tts"), -} - - @pytest.fixture(name="create_backup") def mock_create_backup() -> Generator[AsyncMock]: """Mock manager create backup.""" @@ -125,43 +78,15 @@ def mock_create_backup() -> Generator[AsyncMock]: yield mock_create_backup -@pytest.fixture(name="mock_backup_generation") -def mock_backup_generation_fixture( - hass: HomeAssistant, mocked_json_bytes: Mock, mocked_tarfile: Mock -) -> Generator[None]: - """Mock backup generator.""" +@pytest.fixture(name="mock_ha_version") +def mock_ha_version_fixture(hass: HomeAssistant) -> Generator[None]: + """Mock HA version. - with ( - patch( - "pathlib.Path.iterdir", - lambda x: CONFIG_DIR.get(f"{x.parent.name}/{x.name}", []), - ), - patch("pathlib.Path.stat", return_value=MagicMock(st_size=123)), - patch("pathlib.Path.is_file", lambda x: x not in CONFIG_DIR_DIRS), - patch("pathlib.Path.is_dir", lambda x: x in CONFIG_DIR_DIRS), - patch( - "pathlib.Path.exists", - lambda x: ( - x - not in ( - Path(hass.config.path("backups")), - Path(hass.config.path("tmp_backups")), - ) - ), - ), - patch( - "pathlib.Path.is_symlink", - lambda _: False, - ), - patch( - "pathlib.Path.mkdir", - MagicMock(), - ), - patch( - "homeassistant.components.backup.manager.HAVERSION", - "2025.1.0", - ), - ): + The HA version is included in backup metadata. We mock it for the benefit + of tests that check the exact content of the metadata. + """ + + with patch("homeassistant.components.backup.manager.HAVERSION", "2025.1.0"): yield diff --git a/tests/components/backup/fixtures/config_dir_contents/.storage/hacs.hacs b/tests/components/backup/fixtures/config_dir_contents/.storage/hacs.hacs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/another_subdir/backups/backup.tar b/tests/components/backup/fixtures/config_dir_contents/another_subdir/backups/backup.tar new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/another_subdir/backups/not_backup b/tests/components/backup/fixtures/config_dir_contents/another_subdir/backups/not_backup new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/another_subdir/tts/voice.mp3 b/tests/components/backup/fixtures/config_dir_contents/another_subdir/tts/voice.mp3 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/backups/not_backup b/tests/components/backup/fixtures/config_dir_contents/backups/not_backup new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/home-assistant_v2.db b/tests/components/backup/fixtures/config_dir_contents/home-assistant_v2.db new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/test.txt b/tests/components/backup/fixtures/config_dir_contents/test.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/tmp_backups/forgotten_backup.tar b/tests/components/backup/fixtures/config_dir_contents/tmp_backups/forgotten_backup.tar new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/tmp_backups/not_backup b/tests/components/backup/fixtures/config_dir_contents/tmp_backups/not_backup new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/config_dir_contents/tts/voice.mp3 b/tests/components/backup/fixtures/config_dir_contents/tts/voice.mp3 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/backup/fixtures/test_backups/abc123.tar b/tests/components/backup/fixtures/test_backups/abc123.tar new file mode 100644 index 00000000000..c67b54a1405 Binary files /dev/null and b/tests/components/backup/fixtures/test_backups/abc123.tar differ diff --git a/tests/components/backup/fixtures/test_backups/backup_v2_compressed.tar b/tests/components/backup/fixtures/test_backups/backup_v2_compressed.tar new file mode 100644 index 00000000000..b678d1920e5 Binary files /dev/null and b/tests/components/backup/fixtures/test_backups/backup_v2_compressed.tar differ diff --git a/tests/components/backup/fixtures/test_backups/backup_v2_compressed_protected.tar b/tests/components/backup/fixtures/test_backups/backup_v2_compressed_protected.tar new file mode 100644 index 00000000000..caef8f6131b Binary files /dev/null and b/tests/components/backup/fixtures/test_backups/backup_v2_compressed_protected.tar differ diff --git a/tests/components/backup/fixtures/test_backups/backup_v2_uncompressed.tar b/tests/components/backup/fixtures/test_backups/backup_v2_uncompressed.tar new file mode 100644 index 00000000000..b55a9e6ca4c Binary files /dev/null and b/tests/components/backup/fixtures/test_backups/backup_v2_uncompressed.tar differ diff --git a/tests/components/backup/fixtures/test_backups/backup_v2_uncompressed_protected.tar b/tests/components/backup/fixtures/test_backups/backup_v2_uncompressed_protected.tar new file mode 100644 index 00000000000..2f0db1a4105 Binary files /dev/null and b/tests/components/backup/fixtures/test_backups/backup_v2_uncompressed_protected.tar differ diff --git a/tests/components/backup/fixtures/test_backups/custom_def456.tar b/tests/components/backup/fixtures/test_backups/custom_def456.tar new file mode 100644 index 00000000000..76c1e3e4dd2 Binary files /dev/null and b/tests/components/backup/fixtures/test_backups/custom_def456.tar differ diff --git a/tests/components/backup/snapshots/test_backup.ambr b/tests/components/backup/snapshots/test_backup.ambr index bf6305e8479..5e1ba47327f 100644 --- a/tests/components/backup/snapshots/test_backup.ambr +++ b/tests/components/backup/snapshots/test_backup.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_delete_backup[found_backups0-abc123-1-unlink_path0] +# name: test_delete_backup[available_backups0-abc123-1-unlink_path0] dict({ 'id': 1, 'result': dict({ @@ -10,7 +10,7 @@ 'type': 'result', }) # --- -# name: test_delete_backup[found_backups1-def456-1-unlink_path1] +# name: test_delete_backup[available_backups1-def456-1-unlink_path1] dict({ 'id': 1, 'result': dict({ @@ -21,7 +21,7 @@ 'type': 'result', }) # --- -# name: test_delete_backup[found_backups2-abc123-0-None] +# name: test_delete_backup[available_backups2-abc123-0-None] dict({ 'id': 1, 'result': dict({ @@ -32,7 +32,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[mock_read_backup] +# name: test_load_backups[read_backup-available_backups0] dict({ 'id': 1, 'result': dict({ @@ -47,7 +47,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[mock_read_backup].1 +# name: test_load_backups[read_backup-available_backups0].1 dict({ 'id': 2, 'result': dict({ @@ -65,7 +65,7 @@ 'agents': dict({ 'backup.local': dict({ 'protected': False, - 'size': 0, + 'size': 10240, }), }), 'backup_id': 'abc123', @@ -96,7 +96,7 @@ 'agents': dict({ 'backup.local': dict({ 'protected': False, - 'size': 1, + 'size': 10240, }), }), 'backup_id': 'def456', @@ -133,7 +133,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect1] +# name: test_load_backups[side_effect1-available_backups0] dict({ 'id': 1, 'result': dict({ @@ -148,7 +148,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect1].1 +# name: test_load_backups[side_effect1-available_backups0].1 dict({ 'id': 2, 'result': dict({ @@ -167,7 +167,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect2] +# name: test_load_backups[side_effect2-available_backups0] dict({ 'id': 1, 'result': dict({ @@ -182,7 +182,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect2].1 +# name: test_load_backups[side_effect2-available_backups0].1 dict({ 'id': 2, 'result': dict({ @@ -201,7 +201,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect3] +# name: test_load_backups[side_effect3-available_backups0] dict({ 'id': 1, 'result': dict({ @@ -216,7 +216,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect3].1 +# name: test_load_backups[side_effect3-available_backups0].1 dict({ 'id': 2, 'result': dict({ @@ -235,7 +235,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect4] +# name: test_load_backups[side_effect4-available_backups0] dict({ 'id': 1, 'result': dict({ @@ -250,7 +250,7 @@ 'type': 'result', }) # --- -# name: test_load_backups[side_effect4].1 +# name: test_load_backups[side_effect4-available_backups0].1 dict({ 'id': 2, 'result': dict({ diff --git a/tests/components/backup/test_backup.py b/tests/components/backup/test_backup.py index 0624839336c..c866a7b2f90 100644 --- a/tests/components/backup/test_backup.py +++ b/tests/components/backup/test_backup.py @@ -12,7 +12,7 @@ from unittest.mock import MagicMock, mock_open, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.backup import DOMAIN, AgentBackup +from homeassistant.components.backup import DOMAIN, backup from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -25,30 +25,23 @@ from .common import ( from tests.typing import ClientSessionGenerator, WebSocketGenerator - -def mock_read_backup(backup_path: Path) -> AgentBackup: - """Mock read backup.""" - mock_backups = { - "abc123": TEST_BACKUP_ABC123, - "custom_def456": TEST_BACKUP_DEF456, - } - return mock_backups[backup_path.stem] +real_read_backup = backup.read_backup @pytest.fixture(name="read_backup") -def read_backup_fixture(path_glob: MagicMock) -> Generator[MagicMock]: +def read_backup_fixture() -> Generator[MagicMock]: """Mock read backup.""" - with patch( - "homeassistant.components.backup.backup.read_backup", - side_effect=mock_read_backup, - ) as read_backup: + with patch("homeassistant.components.backup.backup.read_backup") as read_backup: yield read_backup +@pytest.mark.parametrize( + "available_backups", [[TEST_BACKUP_PATH_ABC123, TEST_BACKUP_PATH_DEF456]] +) @pytest.mark.parametrize( "side_effect", [ - mock_read_backup, + real_read_backup, OSError("Boom"), TarError("Boom"), json.JSONDecodeError("Boom", "test", 1), @@ -74,7 +67,11 @@ async def test_load_backups( # load and list backups await client.send_json_auto_id({"type": "backup/info"}) - assert await client.receive_json() == snapshot + response = await client.receive_json() + response["result"]["backups"] = sorted( + response["result"]["backups"], key=lambda b: b["backup_id"] + ) + assert response == snapshot async def test_upload( @@ -106,9 +103,8 @@ async def test_upload( assert move_mock.mock_calls[0].args[1].name == "Test_1970-01-01_00.00_00000000.tar" -@pytest.mark.usefixtures("read_backup") @pytest.mark.parametrize( - ("found_backups", "backup_id", "unlink_calls", "unlink_path"), + ("available_backups", "backup_id", "unlink_calls", "unlink_path"), [ ( [TEST_BACKUP_PATH_ABC123, TEST_BACKUP_PATH_DEF456], @@ -122,7 +118,7 @@ async def test_upload( 1, TEST_BACKUP_PATH_DEF456, ), - (([], TEST_BACKUP_ABC123.backup_id, 0, None)), + ([], TEST_BACKUP_ABC123.backup_id, 0, None), ], ) async def test_delete_backup( @@ -130,8 +126,6 @@ async def test_delete_backup( caplog: pytest.LogCaptureFixture, hass_ws_client: WebSocketGenerator, snapshot: SnapshotAssertion, - path_glob: MagicMock, - found_backups: list[Path], backup_id: str, unlink_calls: int, unlink_path: Path | None, @@ -140,7 +134,6 @@ async def test_delete_backup( assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() client = await hass_ws_client(hass) - path_glob.return_value = found_backups with ( patch("pathlib.Path.unlink", autospec=True) as unlink, @@ -152,4 +145,4 @@ async def test_delete_backup( assert unlink.call_count == unlink_calls for call in unlink.mock_calls: - assert call.args[0] == unlink_path + assert Path(call.args[0].name) == unlink_path diff --git a/tests/components/backup/test_event.py b/tests/components/backup/test_event.py index dc7f57018bb..3cb49f5ecb5 100644 --- a/tests/components/backup/test_event.py +++ b/tests/components/backup/test_event.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock, patch -import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup.const import DOMAIN @@ -18,7 +17,6 @@ from tests.common import snapshot_platform from tests.typing import WebSocketGenerator -@pytest.mark.usefixtures("mock_backup_generation") async def test_event_entity( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -34,7 +32,6 @@ async def test_event_entity( await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) -@pytest.mark.usefixtures("mock_backup_generation") async def test_event_entity_backup_completed( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -66,7 +63,6 @@ async def test_event_entity_backup_completed( assert state.attributes[ATTR_FAILED_REASON] is None -@pytest.mark.usefixtures("mock_backup_generation") async def test_event_entity_backup_failed( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index 0d5bdfd6504..a82981d10c5 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -4,6 +4,7 @@ import asyncio from collections.abc import AsyncIterator from io import BytesIO, StringIO import json +from pathlib import Path import re import tarfile from typing import Any @@ -23,7 +24,12 @@ from homeassistant.components.backup import ( from homeassistant.components.backup.const import DOMAIN from homeassistant.core import HomeAssistant -from .common import TEST_BACKUP_ABC123, aiter_from_iter, setup_backup_integration +from .common import ( + TEST_BACKUP_ABC123, + TEST_BACKUP_PATH_ABC123, + aiter_from_iter, + setup_backup_integration, +) from tests.common import MockUser, get_fixture_path from tests.typing import ClientSessionGenerator @@ -43,6 +49,7 @@ PROTECTED_BACKUP = AgentBackup( ) +@pytest.mark.parametrize("available_backups", [[TEST_BACKUP_PATH_ABC123]]) async def test_downloading_local_backup( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -52,22 +59,8 @@ async def test_downloading_local_backup( client = await hass_client() - with ( - patch( - "homeassistant.components.backup.backup.CoreLocalBackupAgent.async_get_backup", - return_value=TEST_BACKUP_ABC123, - ), - patch( - "homeassistant.components.backup.backup.CoreLocalBackupAgent.get_backup_path", - ), - 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 + resp = await client.get("/api/backup/download/abc123?agent_id=backup.local") + assert resp.status == 200 async def test_downloading_remote_backup( @@ -87,27 +80,21 @@ async def test_downloading_remote_backup( assert await resp.content.read() == b"backup data" +@pytest.mark.parametrize("available_backups", [[TEST_BACKUP_PATH_ABC123]]) async def test_downloading_local_encrypted_backup_file_not_found( hass: HomeAssistant, hass_client: ClientSessionGenerator, ) -> None: - """Test downloading a local backup file.""" + """Test downloading a missing 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( - "homeassistant.components.backup.backup.CoreLocalBackupAgent.get_backup_path", - ), - ): - resp = await client.get( - "/api/backup/download/abc123?agent_id=backup.local&password=blah" - ) - assert resp.status == 404 + Path(hass.config.path("backups/abc123.tar")).unlink() + + resp = await client.get( + "/api/backup/download/abc123?agent_id=backup.local&password=blah" + ) + assert resp.status == 404 @pytest.mark.usefixtures("mock_backups") @@ -241,7 +228,7 @@ async def test_downloading_backup_not_found( client = await hass_client() - resp = await client.get("/api/backup/download/abc123?agent_id=backup.local") + resp = await client.get("/api/backup/download/abc1234?agent_id=backup.local") assert resp.status == 404 diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index f641ce75867..67cc4e1b3e7 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -24,6 +24,7 @@ from unittest.mock import ( from freezegun.api import FrozenDateTimeFactory import pytest +from securetar import SecureTarFile from homeassistant.components.backup import ( DOMAIN, @@ -70,6 +71,7 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator _EXPECTED_FILES = [ "test.txt", ".storage", + ".storage/hacs.hacs", "another_subdir", "another_subdir/backups", "another_subdir/backups/backup.tar", @@ -112,12 +114,10 @@ def mock_read_backup(backup_path: Path) -> AgentBackup: return mock_backups[backup_path.stem] -@pytest.mark.usefixtures("mock_backup_generation") +@pytest.mark.usefixtures("mock_ha_version") async def test_create_backup_service( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mocked_json_bytes: Mock, - mocked_tarfile: Mock, ) -> None: """Test create backup service.""" await setup_backup_integration(hass) @@ -161,7 +161,7 @@ async def test_create_backup_service( ) -@pytest.mark.usefixtures("mock_backup_generation") +@pytest.mark.usefixtures("mock_ha_version") @pytest.mark.parametrize( ("manager_kwargs", "expected_writer_kwargs"), [ @@ -312,8 +312,6 @@ async def test_create_backup_service( async def test_async_create_backup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mocked_json_bytes: Mock, - mocked_tarfile: Mock, manager_kwargs: dict[str, Any], expected_writer_kwargs: dict[str, Any], ) -> None: @@ -342,7 +340,6 @@ async def test_async_create_backup( assert create_backup.call_args == call(**expected_writer_kwargs) -@pytest.mark.usefixtures("mock_backup_generation") async def test_create_backup_when_busy( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -419,7 +416,7 @@ async def test_create_backup_wrong_parameters( assert result["error"]["message"] == expected_error -@pytest.mark.usefixtures("mock_backup_generation") +@pytest.mark.usefixtures("mock_ha_version") @pytest.mark.parametrize( ( "agent_ids", @@ -519,9 +516,7 @@ async def test_initiate_backup( hass_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, mocked_json_bytes: Mock, - mocked_tarfile: Mock, generate_backup_id: MagicMock, - path_glob: MagicMock, params: dict[str, Any], agent_ids: list[str], backup_directory: str, @@ -540,7 +535,6 @@ async def test_initiate_backup( include_database = params.get("include_database", True) password = params.get("password") - path_glob.return_value = [] await ws_client.send_json_auto_id({"type": "backup/info"}) result = await ws_client.receive_json() @@ -664,26 +658,31 @@ async def test_initiate_backup( "with_automatic_settings": False, } - outer_tar = mocked_tarfile.return_value - core_tar = outer_tar.create_inner_tar.return_value.__enter__.return_value - expected_files = [call(hass.config.path(), arcname="data", recursive=False)] + [ - call(file, arcname=f"data/{file}", recursive=False) - for file in _EXPECTED_FILES_WITH_DATABASE[include_database] - ] - assert core_tar.add.call_args_list == expected_files + expected_files = { + f"data/{file}" for file in _EXPECTED_FILES_WITH_DATABASE[include_database] + } + expected_files.add("data") - tar_file_path = str(mocked_tarfile.call_args_list[0][0][0]) - backup_directory = hass.config.path(backup_directory) - assert tar_file_path == f"{backup_directory}/{expected_filename}" + with tarfile.TarFile( + hass.config.path(f"{backup_directory}/{expected_filename}"), mode="r" + ) as outer_tar: + core_tar_io = outer_tar.extractfile("homeassistant.tar.gz") + assert core_tar_io is not None + with SecureTarFile( + fileobj=core_tar_io, + gzip=True, + key=password_to_key(password) if password is not None else None, + mode="r", + ) as core_tar: + assert set(core_tar.getnames()) == expected_files -@pytest.mark.usefixtures("mock_backup_generation") +@pytest.mark.usefixtures("mock_ha_version") @pytest.mark.parametrize("exception", [BackupAgentError("Boom!"), Exception("Boom!")]) async def test_initiate_backup_with_agent_error( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, generate_backup_id: MagicMock, - path_glob: MagicMock, hass_storage: dict[str, Any], exception: Exception, ) -> None: @@ -781,8 +780,6 @@ async def test_initiate_backup_with_agent_error( 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() @@ -861,7 +858,7 @@ async def test_initiate_backup_with_agent_error( new_expected_backup_data = { "addons": [], - "agents": {"backup.local": {"protected": False, "size": 123}}, + "agents": {"backup.local": {"protected": False, "size": 10240}}, "backup_id": "abc123", "database_included": True, "date": ANY, @@ -911,7 +908,6 @@ async def test_initiate_backup_with_agent_error( assert mock_agents["test.remote"].async_delete_backup.call_count == 1 -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ("create_backup_command", "issues_after_create_backup"), [ @@ -1332,7 +1328,6 @@ async def test_create_backup_failure_raises_issue( assert issue.translation_placeholders == issue_data["translation_placeholders"] -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( "exception", [BackupReaderWriterError("Boom!"), BaseException("Boom!")] ) @@ -1340,7 +1335,6 @@ async def test_initiate_backup_non_agent_upload_error( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, generate_backup_id: MagicMock, - path_glob: MagicMock, hass_storage: dict[str, Any], exception: Exception, ) -> None: @@ -1350,8 +1344,6 @@ async def test_initiate_backup_non_agent_upload_error( 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() @@ -1425,7 +1417,6 @@ async def test_initiate_backup_non_agent_upload_error( assert DOMAIN not in hass_storage -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( "exception", [BackupReaderWriterError("Boom!"), Exception("Boom!")] ) @@ -1433,7 +1424,6 @@ async def test_initiate_backup_with_task_error( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, generate_backup_id: MagicMock, - path_glob: MagicMock, create_backup: AsyncMock, exception: Exception, ) -> None: @@ -1447,8 +1437,6 @@ async def test_initiate_backup_with_task_error( 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() @@ -1503,7 +1491,6 @@ async def test_initiate_backup_with_task_error( assert backup_id == generate_backup_id.return_value -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ( "open_call_count", @@ -1526,7 +1513,6 @@ async def test_initiate_backup_file_error_upload_to_agents( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, generate_backup_id: MagicMock, - path_glob: MagicMock, open_call_count: int, open_exception: Exception | None, read_call_count: int, @@ -1543,8 +1529,6 @@ async def test_initiate_backup_file_error_upload_to_agents( 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() @@ -1629,7 +1613,6 @@ async def test_initiate_backup_file_error_upload_to_agents( assert unlink_mock.call_count == unlink_call_count -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ( "mkdir_call_count", @@ -1650,7 +1633,6 @@ async def test_initiate_backup_file_error_create_backup( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, generate_backup_id: MagicMock, - path_glob: MagicMock, caplog: pytest.LogCaptureFixture, mkdir_call_count: int, mkdir_exception: Exception | None, @@ -1667,8 +1649,6 @@ async def test_initiate_backup_file_error_create_backup( 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() @@ -1879,7 +1859,6 @@ async def test_exception_platform_pre(hass: HomeAssistant) -> None: ), ], ) -@pytest.mark.usefixtures("mock_backup_generation") async def test_exception_platform_post( hass: HomeAssistant, unhandled_error: Exception | None, @@ -2004,7 +1983,6 @@ async def test_receive_backup( assert unlink_mock.call_count == temp_file_unlink_call_count -@pytest.mark.usefixtures("mock_backup_generation") async def test_receive_backup_busy_manager( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -2068,13 +2046,11 @@ async def test_receive_backup_busy_manager( 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: @@ -2172,8 +2148,6 @@ async def test_receive_backup_agent_error( 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() @@ -2294,13 +2268,11 @@ async def test_receive_backup_agent_error( assert mock_agents["test.remote"].async_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: @@ -2310,8 +2282,6 @@ async def test_receive_backup_non_agent_upload_error( 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() @@ -2388,7 +2358,6 @@ async def test_receive_backup_non_agent_upload_error( assert unlink_mock.call_count == 0 -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ( "open_call_count", @@ -2408,7 +2377,6 @@ 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, @@ -2422,8 +2390,6 @@ async def test_receive_backup_file_write_error( 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() @@ -2495,7 +2461,6 @@ async def test_receive_backup_file_write_error( assert open_mock.return_value.close.call_count == close_call_count -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( "exception", [ @@ -2509,7 +2474,6 @@ 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.""" @@ -2518,8 +2482,6 @@ async def test_receive_backup_read_tar_error( 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() @@ -2590,7 +2552,6 @@ async def test_receive_backup_read_tar_error( assert read_backup.call_count == 1 -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ( "open_call_count", @@ -2664,7 +2625,6 @@ 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, @@ -2683,8 +2643,6 @@ async def test_receive_backup_file_read_error( 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() @@ -2771,7 +2729,9 @@ async def test_receive_backup_file_read_error( assert unlink_mock.call_count == unlink_call_count -@pytest.mark.usefixtures("path_glob") +@pytest.mark.parametrize( + "available_backups", [[TEST_BACKUP_PATH_ABC123, TEST_BACKUP_PATH_DEF456]] +) @pytest.mark.parametrize( ( "agent_id", @@ -2921,7 +2881,7 @@ async def test_restore_backup( assert mocked_service_call.called -@pytest.mark.usefixtures("path_glob") +@pytest.mark.parametrize("available_backups", [[TEST_BACKUP_PATH_ABC123]]) @pytest.mark.parametrize( ("agent_id", "dir"), [(LOCAL_AGENT_ID, "backups"), ("test.remote", "tmp_backups")] ) @@ -3001,7 +2961,7 @@ async def test_restore_backup_wrong_password( mocked_service_call.assert_not_called() -@pytest.mark.usefixtures("path_glob") +@pytest.mark.parametrize("available_backups", [[TEST_BACKUP_PATH_ABC123]]) @pytest.mark.parametrize( ("parameters", "expected_error", "expected_reason"), [ @@ -3093,7 +3053,6 @@ async def test_restore_backup_wrong_parameters( mocked_service_call.assert_not_called() -@pytest.mark.usefixtures("mock_backup_generation") async def test_restore_backup_when_busy( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -3123,7 +3082,6 @@ async def test_restore_backup_when_busy( assert result["error"]["message"] == "Backup manager busy: create_backup" -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ("exception", "error_code", "error_message", "expected_reason"), [ @@ -3208,7 +3166,6 @@ async def test_restore_backup_agent_error( assert mocked_service_call.call_count == 0 -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ( "open_call_count", @@ -3353,6 +3310,7 @@ async def test_restore_backup_file_error( assert mocked_service_call.call_count == 0 +@pytest.mark.usefixtures("mock_ha_version") @pytest.mark.parametrize( ("commands", "agent_ids", "password", "protected_backup", "inner_tar_key"), [ @@ -3477,13 +3435,10 @@ async def test_restore_backup_file_error( ), ], ) -@pytest.mark.usefixtures("mock_backup_generation") async def test_initiate_backup_per_agent_encryption( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, generate_backup_id: MagicMock, - mocked_tarfile: Mock, - path_glob: MagicMock, commands: dict[str, Any], agent_ids: list[str], password: str | None, @@ -3495,8 +3450,6 @@ async def test_initiate_backup_per_agent_encryption( 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 @@ -3526,6 +3479,7 @@ async def test_initiate_backup_per_agent_encryption( with ( patch("pathlib.Path.open", mock_open(read_data=b"test")), + patch("securetar.SecureTarFile.create_inner_tar") as mock_create_inner_tar, ): await ws_client.send_json_auto_id( { @@ -3550,9 +3504,7 @@ async def test_initiate_backup_per_agent_encryption( await hass.async_block_till_done() - mocked_tarfile.return_value.create_inner_tar.assert_called_once_with( - ANY, gzip=True, key=inner_tar_key - ) + mock_create_inner_tar.assert_called_once_with(ANY, gzip=True, key=inner_tar_key) result = await ws_client.receive_json() assert result["event"] == { diff --git a/tests/components/backup/test_sensors.py b/tests/components/backup/test_sensors.py index 7320c037b21..2f78d0d1f70 100644 --- a/tests/components/backup/test_sensors.py +++ b/tests/components/backup/test_sensors.py @@ -4,7 +4,6 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from freezegun.api import FrozenDateTimeFactory -import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup import store @@ -19,7 +18,6 @@ from tests.common import async_fire_time_changed, snapshot_platform from tests.typing import WebSocketGenerator -@pytest.mark.usefixtures("mock_backup_generation") async def test_sensors( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, diff --git a/tests/components/backup/test_util.py b/tests/components/backup/test_util.py index af37a3b88a6..01909793067 100644 --- a/tests/components/backup/test_util.py +++ b/tests/components/backup/test_util.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncIterator import dataclasses +from pathlib import Path import tarfile from unittest.mock import Mock, patch @@ -129,22 +130,39 @@ def test_read_backup(backup_json_content: bytes, expected_backup: AgentBackup) - assert backup == expected_backup -@pytest.mark.parametrize("password", [None, "hunter2"]) -def test_validate_password(password: str | None) -> None: +@pytest.mark.parametrize( + ("backup", "password", "validation_result"), + [ + # Backup not protected, no password provided -> validation passes + (Path("backup_v2_compressed.tar"), None, True), + (Path("backup_v2_uncompressed.tar"), None, True), + # Backup not protected, password provided -> validation fails + (Path("backup_v2_compressed.tar"), "hunter2", False), + (Path("backup_v2_uncompressed.tar"), "hunter2", False), + # Backup protected, correct password provided -> validation passes + (Path("backup_v2_compressed_protected.tar"), "hunter2", True), + (Path("backup_v2_uncompressed_protected.tar"), "hunter2", True), + # Backup protected, no password provided -> validation fails + (Path("backup_v2_compressed_protected.tar"), None, False), + (Path("backup_v2_uncompressed_protected.tar"), None, False), + # Backup protected, wrong password provided -> validation fails + (Path("backup_v2_compressed_protected.tar"), "wrong_password", False), + (Path("backup_v2_uncompressed_protected.tar"), "wrong_password", False), + ], +) +def test_validate_password( + password: str | None, backup: Path, validation_result: bool +) -> None: """Test validating a password.""" - mock_path = Mock() + test_backups = get_fixture_path("test_backups", DOMAIN) - with ( - patch("homeassistant.components.backup.util.tarfile.open"), - patch("homeassistant.components.backup.util.SecureTarFile"), - ): - assert validate_password(mock_path, password) is True + assert validate_password(test_backups / backup, password) == validation_result @pytest.mark.parametrize("password", [None, "hunter2"]) @pytest.mark.parametrize("secure_tar_side_effect", [tarfile.ReadError, Exception]) -def test_validate_password_wrong_password( - password: str | None, secure_tar_side_effect: Exception +def test_validate_password_with_error( + password: str | None, secure_tar_side_effect: type[Exception] ) -> None: """Test validating a password.""" mock_path = Mock() diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index ba19abdbb34..590cd48875e 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -403,6 +403,7 @@ async def test_agent_delete_backup( assert mock_agents["test.remote"].async_delete_backup.call_args == call("abc123") +@pytest.mark.usefixtures("mock_ha_version") @pytest.mark.parametrize( "data", [ @@ -411,7 +412,6 @@ async def test_agent_delete_backup( {"password": "abc123"}, ], ) -@pytest.mark.usefixtures("mock_backup_generation") async def test_generate( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -478,7 +478,6 @@ async def test_generate_wrong_parameters( } -@pytest.mark.usefixtures("mock_backup_generation") @pytest.mark.parametrize( ("params", "expected_extra_call_params"), [