From 94b80db96816939575636e6197e835b332cc2bc7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 3 Jan 2023 02:28:21 +0100 Subject: [PATCH] Handle not available add-on in hassio add-on manager (#84943) * Handle not available add-on in hassio add-on manager * Fix zwave_js tests * Fix sky connect tests * Fix matter tests * Fix yellow tests * Update hardware tests --- .../components/hassio/addon_manager.py | 11 +++++ tests/components/hassio/test_addon_manager.py | 44 ++++++++++++++++++- .../homeassistant_hardware/conftest.py | 2 + .../homeassistant_sky_connect/conftest.py | 2 + .../homeassistant_yellow/conftest.py | 2 + tests/components/matter/conftest.py | 9 ++++ tests/components/matter/test_init.py | 2 +- tests/components/zwave_js/conftest.py | 11 +++++ tests/components/zwave_js/test_config_flow.py | 1 + 9 files changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index f240937c7f5..46eca080b1b 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -70,6 +70,7 @@ def api_error( class AddonInfo: """Represent the current add-on info state.""" + available: bool hostname: str | None options: dict[str, Any] state: AddonState @@ -144,6 +145,7 @@ class AddonManager: self._logger.debug("Add-on store info: %s", addon_store_info) if not addon_store_info["installed"]: return AddonInfo( + available=addon_store_info["available"], hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -154,6 +156,7 @@ class AddonManager: addon_info = await async_get_addon_info(self._hass, self.addon_slug) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( + available=addon_info["available"], hostname=addon_info["hostname"], options=addon_info["options"], state=addon_state, @@ -184,6 +187,11 @@ class AddonManager: @api_error("Failed to install the {addon_name} add-on") async def async_install_addon(self) -> None: """Install the managed add-on.""" + addon_info = await self.async_get_addon_info() + + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + await async_install_addon(self._hass, self.addon_slug) @api_error("Failed to uninstall the {addon_name} add-on") @@ -196,6 +204,9 @@ class AddonManager: """Update the managed add-on if needed.""" addon_info = await self.async_get_addon_info() + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + if addon_info.state is AddonState.NOT_INSTALLED: raise AddonError(f"{self.addon_name} add-on is not installed") diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 7135f1ea646..5ee7856b9f7 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -32,6 +32,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -41,10 +42,12 @@ def mock_addon_installed( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-test-addon" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -67,6 +70,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +85,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -180,6 +185,26 @@ async def test_not_installed_raises_exception( assert str(err.value) == "Test add-on is not installed" +async def test_not_available_raises_exception( + addon_manager: AddonManager, + addon_store_info: AsyncMock, + addon_info: AsyncMock, +) -> None: + """Test addon not available raises exception.""" + addon_store_info.return_value["available"] = False + addon_info.return_value["available"] = False + + with pytest.raises(AddonError) as err: + await addon_manager.async_install_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + async def test_get_addon_discovery_info( addon_manager: AddonManager, get_addon_discovery_info: AsyncMock ) -> None: @@ -222,6 +247,7 @@ async def test_get_addon_info_not_installed( ) -> None: """Test get addon info when addon is not installed..""" assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -243,6 +269,7 @@ async def test_get_addon_info( """Test get addon info when addon is installed.""" addon_installed.return_value["state"] = addon_info_state assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=addon_state, @@ -308,18 +335,29 @@ async def test_set_addon_options_error( async def test_install_addon( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True + await addon_manager.async_install_addon() assert install_addon.call_count == 1 async def test_install_addon_error( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon raises error.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True install_addon.side_effect = HassioAPIError("Boom") with pytest.raises(AddonError) as err: @@ -341,6 +379,7 @@ async def test_schedule_install_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.INSTALLING, @@ -676,6 +715,7 @@ async def test_schedule_update_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.UPDATING, diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index fd0ce2e761b..4add48781a9 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 2d333c62b2d..f7f0bb8d128 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -69,6 +69,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -83,6 +84,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 62595c11fe1..bc48c6b01fd 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 8310d725c8e..486e2fd26ac 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -80,6 +80,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -94,6 +95,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -108,6 +110,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -117,10 +120,12 @@ def addon_installed_fixture( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -133,10 +138,12 @@ def addon_running_fixture( ) -> AsyncMock: """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" @@ -152,10 +159,12 @@ def install_addon_fixture( async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None: """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index fbc538016dc..a3febe799a5 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -307,7 +307,7 @@ async def test_install_addon( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY - assert addon_store_info.call_count == 2 + assert addon_store_info.call_count == 3 assert install_addon.call_count == 1 assert install_addon.call_args == call(hass, "core_matter_server") assert start_addon.call_count == 1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index ba97cfe4c36..0b0503a3e29 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -30,6 +30,7 @@ def mock_addon_info(addon_info_side_effect): side_effect=addon_info_side_effect, ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -53,6 +54,7 @@ def mock_addon_store_info(addon_store_info_side_effect): side_effect=addon_store_info_side_effect, ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -64,10 +66,12 @@ def mock_addon_store_info(addon_store_info_side_effect): def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -77,10 +81,12 @@ def mock_addon_running(addon_store_info, addon_info): def mock_addon_installed(addon_store_info, addon_info): """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -89,6 +95,7 @@ def mock_addon_installed(addon_store_info, addon_info): @pytest.fixture(name="addon_not_installed") def mock_addon_not_installed(addon_store_info, addon_info): """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -126,10 +133,12 @@ def install_addon_side_effect_fixture(addon_store_info, addon_info): async def install_addon(hass, slug): """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -162,10 +171,12 @@ def start_addon_side_effect_fixture(addon_store_info, addon_info): async def start_addon(hass, slug): """Mock start add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "started" return start_addon diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index eacf4b61cc8..2bff9c2cccb 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -536,6 +536,7 @@ async def test_abort_hassio_discovery_for_other_addon( async def test_usb_discovery( hass, supervisor, + addon_not_installed, install_addon, addon_options, get_addon_discovery_info,