From e16a8ed20e2913a11892378b4b02ecf977e76246 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 Feb 2026 21:39:34 +0100 Subject: [PATCH] Don't mock out filesystem operations in backup tests (#162877) --- tests/components/backup/conftest.py | 137 ++++-------------- .../config_dir_contents/.storage/hacs.hacs | 0 .../another_subdir/backups/backup.tar | 0 .../another_subdir/backups/not_backup | 0 .../another_subdir/tts/voice.mp3 | 0 .../config_dir_contents/backups/not_backup | 0 .../config_dir_contents/home-assistant_v2.db | 0 .../fixtures/config_dir_contents/test.txt | 0 .../tmp_backups/forgotten_backup.tar | 0 .../tmp_backups/not_backup | 0 .../config_dir_contents/tts/voice.mp3 | 0 .../backup/fixtures/test_backups/abc123.tar | Bin 0 -> 10240 bytes .../test_backups/backup_v2_compressed.tar | Bin 0 -> 10240 bytes .../backup_v2_compressed_protected.tar | Bin 0 -> 10240 bytes .../test_backups/backup_v2_uncompressed.tar | Bin 0 -> 30720 bytes .../backup_v2_uncompressed_protected.tar | Bin 0 -> 20480 bytes .../fixtures/test_backups/custom_def456.tar | Bin 0 -> 10240 bytes .../backup/snapshots/test_backup.ambr | 30 ++-- tests/components/backup/test_backup.py | 39 ++--- tests/components/backup/test_event.py | 4 - tests/components/backup/test_http.py | 51 +++---- tests/components/backup/test_manager.py | 110 ++++---------- tests/components/backup/test_sensors.py | 2 - tests/components/backup/test_util.py | 38 +++-- tests/components/backup/test_websocket.py | 3 +- 25 files changed, 141 insertions(+), 273 deletions(-) create mode 100644 tests/components/backup/fixtures/config_dir_contents/.storage/hacs.hacs create mode 100644 tests/components/backup/fixtures/config_dir_contents/another_subdir/backups/backup.tar create mode 100644 tests/components/backup/fixtures/config_dir_contents/another_subdir/backups/not_backup create mode 100644 tests/components/backup/fixtures/config_dir_contents/another_subdir/tts/voice.mp3 create mode 100644 tests/components/backup/fixtures/config_dir_contents/backups/not_backup create mode 100644 tests/components/backup/fixtures/config_dir_contents/home-assistant_v2.db create mode 100644 tests/components/backup/fixtures/config_dir_contents/test.txt create mode 100644 tests/components/backup/fixtures/config_dir_contents/tmp_backups/forgotten_backup.tar create mode 100644 tests/components/backup/fixtures/config_dir_contents/tmp_backups/not_backup create mode 100644 tests/components/backup/fixtures/config_dir_contents/tts/voice.mp3 create mode 100644 tests/components/backup/fixtures/test_backups/abc123.tar create mode 100644 tests/components/backup/fixtures/test_backups/backup_v2_compressed.tar create mode 100644 tests/components/backup/fixtures/test_backups/backup_v2_compressed_protected.tar create mode 100644 tests/components/backup/fixtures/test_backups/backup_v2_uncompressed.tar create mode 100644 tests/components/backup/fixtures/test_backups/backup_v2_uncompressed_protected.tar create mode 100644 tests/components/backup/fixtures/test_backups/custom_def456.tar 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 0000000000000000000000000000000000000000..c67b54a14051c6bebd210028147bd30fa0993f8f GIT binary patch literal 10240 zcmeIwQBT4!5C?ER3hA@XZh}NU2Tu~S99x0vwl3{eF@*2#b-F0L^TfYtvh~{auD>pO z*iiKz`|K^aZgQQbr9I7Nlk}8!`PB2pe2o3f=en5Zlc~N=Ip@2^z~m+UdGuyRM@4n* zx}cWR^-*=y8hKMI6Ik6%RcPWajrn){VSKQzOQAf|nO3J)Syj2V_a+2W%LMyqBvum- zJLS&{eXn!rE3KD9&*YL{((dNKp5&9GvYp&QSDDhLEtHGCj8WcY(Wm8oNs(PkY^yRD zX4~yjy(!a4R%~5UOI;FKtJc((hRXNkjY`$+U27-=n=1O^i925c& zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|xQ4(t2L^^I literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b678d1920e5381769dee41024dcbfbf03854a8b7 GIT binary patch literal 10240 zcmdPXPfASAE-lc@D$dVipbanp0y7g61`rJd=(K^Ep)pJhB5!D5WMagiU_cw^pqg4* zT#{G>v>sJ-#PF(>5>rz0^NN+M6r!t@@)C1Xfh?tv)Z!8)9R;Q0oYHghPiDTyUulMF4*4Rj3*fhfelzzU4? zfRP*pGCj4Tq$m-nrCKR74;Tx1$*J*~AS0CWON-)5OMz^lqVmj=jQGUTlKkAnlFa1z z;?$Co%)E4vdqFnU0#&5t=cEAL1r&)^%1uqlOa!|;Be5t|3FNwr{M^*U;^It@4JBaP zQY(^kN>ftffo@DpN-PGsGc7Tv802gu4;ldzp`M`;$df<`^pGhi$}dSxE&+QPrWqDm zkQ^0XW)zPUGC(s*Dht42Qjl0wl9`wTawft)quK#Ub9(xE`VIk!6&|UHz(Ayp6F`|7 z2w-^}oi;Eq#?1eQh71Z7v~doqsUtl98<{KQmSpCp+8Uah8(12eT38zE8Ce>eTN-kq zhSMl-ScU*f>8J;+3iZ;fsO?cu=Eqn6n;RK07@8WI7@HWF7?_zd7#JEE7@CdN|J05L zZ05;#b8wh)^kp*q$4ChbZ)bRCA94_AC_cCT#;e-Ro6lEhY*}!@d*POQ-d8+==Bo(4 z+5djswy5YUm)&lkaZhj(N#mHAKWU+0@4I=EzB2{Hhk=m1gcVgL(HL|C? zT|UP-EOn}gS*`5ObpmpeBd4Cf6m8H}DskB@Sdd$9E02Dx^dotxpXVRWx~1_{N#f}K z3CGbn}Ekm*I)c+ z`*3IXd%JHha-JBrXJ_d?E&QL&;UL8sl)ZM#+xiXf6&Y79RhNGE-|VG2>r1szp|-y| z`kl7--q0!!NmB%%+rfIbPXFXRx=YRa?`OK4pw56i|GjZg6WqY9V zKWg9kWk2uSuDD(EU$XeuZ-1%ZELv(i_wqFriweB!VvY~3z4YN{|3CNti~r9)?Y41V zd4u%-`M3Ye{+OS+u;70<`}K$QKc3d7UHD&m;A;Qh`9UG2EoWZx5HS_D;YwuG(+icN`m1$DoXi|v# z!4T+Scb!24TMUnijfTKz2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD hjE2By2#kinXb6mkz-S1JhQMeDjE2By2#k~v005MOrzQXZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..caef8f6131bccb326c40cbf16b486562c7e5587b GIT binary patch literal 10240 zcmdPXPfASAE-lc@D$dVipbanp0y7g61`rJd=(K^Ep%F|BB5!D5WMasmU_cw^pqg4* zT#{G>v>sJ-#PF(>5>rz0^NN+M6r!t@@)C1Xfh?tv)Z!8)9R;Q0oYHghPiDTyUulMF4*4Rj3*fhfelzzU4? zfRP*pGCj4Tq$m-nrCKR74;Tx1$*J*~AS0CWON-)5OMz^lqVmj=jQGUTlKkAnlFa1z z;?$Co%)E4vdqFnU0#&5t=cEAL1r&)^%1uqlOa!|;Be5t|3FNwr{M^*U;^It@4JBaP zQY(^kN>ftffo@DpN-PGsGc7Tv802gu4;ldzp`M`;$df<`^pGhi$}dSxE1C!+R^!4-|0un1cQWJrJNEAzqaLs-)Jw0Twm(6c8ejcyZeqY-XliI;Y+_(yU}nl-U%9Nhu-Vm?U9RuHH%TYx_S!ZU)g9Ztedf7o zo#cD=Lt;<)x6D`XGPl36p7;IXUH=`~>YWbna~>Ty9artr;kQdI#$@{@6Ay#-Ma3B# zL^Twy%v#SO#`;F#ZPL@@@#mYhgQdFfx%uBZC;MEP^@rWD|AuyEPKRxIzs?d4zn^6J zAgRsO)bqJ~|AS2v^GwtewprSISZO`Mhez##kdDjB(l<;@1)EaNf7}plW%Bf#KxWmn zRf|s=-BxCQAz|shOnpPUO!KcPnX@k&OyWEC%=c!NtpC#84S~P^|I|>u^lKAU8t}<=fo$q%iNze%r_T*FgeJm z|3{JEleK@w9<7TPFIL}hsJG+)!&{^%5V2@anO@yJP9wt)7BP~W|8KeRJ9FI{W>*%8 zElus5w@>HSOGd=m*8Sh)m;Qc2P~fGXJx@cM7uH3A9ueYLX)7xUyzg0U&wkWQ7 z%)aIAgH(Yp+io)P`~N)HHz#Dxja5Io++guIN{@!XXb6mkz-S1JhQMeDjE2By2#kin yXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb24d5C8znW1DjT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b55a9e6ca4caad6feaff89c6d54dfaca287d4dd0 GIT binary patch literal 30720 zcmeI2ZEw>$6vy+rpCW{};QA>}i-ZIcNcYW^5bV`OsPbBm2AZUb9T=MS-8pX8-L@** z1Pj{3--?vRcI@MybN+Tt%7({w`n6lKFTSaheiEl-v`*rg(*BtnVK#Z|a>1p}la%*k z&Tgxn54NT5cXJ=j)mdg|v!?dhi}d4JR@=(auk6VAkUdK?zvw>4>u_=X&9%O4>X;=M zXFNOqM75?(wQQaDZbk(scHtdm&TLpUCB_AR&IKJuoWEEdjAr@2*t|Opt)+X;va+UW zsHbjH#x}C1YbRYtzp2vqGR!Bo3r%H1Ih}YHLRo)~28XtD=TzgQS2uq(E@x|y4hjrQ33Xy?Z4tr1Fk3Ym~`l+?;Tnq}JrHw`;u z-r!oh)a09-boCY-Y9;(wuBa?+8%n!~hW_89JpcQT9UHRYt9SPFA7_bSKmA9irnK?# z{WIt4QXAIw$p3{*($jv-*=_Za`@bBeRVXX>QWOOri+nUzOpXmVBR$v!g{y~1U`HHT zTncYLMW_Dt`ma^g29XP`G}qkZ2^T^YSpRQ-A`Y|&{eRs1zg_(&KP?PXBPF$g{vU9c z2baZ${SPVXf02`}b1sTpZl7aOOl$pD+9)3Pf<>WtlIH!JmL!|~pKHsnSM_&Y`BnGc z;WFqy^#5Z2ugu6Op~tL~aP^zJ(Y>u1MSkLcREx5{^7ZFW z1{l}>qA0fC`!@T4SQJxS|3#s4t|h%63MD1Q|70{y{7KI@xEJq#`2O>VzyEH<{}lB9 z272+ps;nfo=NMoV_-o^TJNnO!BK=4Fum6YbZtVX>+#75Xt^ZtddjH4w|NiG+=9l09 z*YUqRXZcu-u=($w{axr`t^a=7mdn8Wycd(AV_&KNB=(o_KSSStgv^CP{I8eE*lYCZ zli_h$5B+>FaMO9S{_9`*Vf8#@dv$RM0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& p00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAn^MHegZZgrtAO! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2f0db1a4105a770e63da0a3406a002505314dd4f GIT binary patch literal 20480 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 W;GMubfp-G$1l|d}6Zjt`@V@{}Fv=YO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..76c1e3e4dd2b5fb208d8b7006f8faff0b4e733d0 GIT binary patch literal 10240 zcmeIwZEM0X5C?ESisbdyrA1})bNFgwl(RiGvrS4KrbGPhyL21Fy%z@lL!jh($z6VJ z@vx?5pN8TsI6Jw{&@vXY*(5(@{C(gUNoxwZ*YD2X 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"), [