Remaining addon management to aiohasupervisor (#128484)

* Move set addon options to aiohasupervisor

* addon stats to aiohasupervisor and test fixes

* addon changelogs to aiohasupervisor

* Raise correct error for library in tests

* Cache client in instance property

* Use singleton method rather then HassIO instance method

* Mock supervisor client in more tests
This commit is contained in:
Mike Degatano 2024-10-21 10:41:00 -04:00 committed by GitHub
parent 9b3ac49298
commit ad55c9cc19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 384 additions and 495 deletions

View File

@ -105,10 +105,8 @@ from .handler import ( # noqa: F401
async_get_green_settings, async_get_green_settings,
async_get_yellow_settings, async_get_yellow_settings,
async_reboot_host, async_reboot_host,
async_set_addon_options,
async_set_green_settings, async_set_green_settings,
async_set_yellow_settings, async_set_yellow_settings,
async_update_addon,
async_update_core, async_update_core,
async_update_diagnostics, async_update_diagnostics,
async_update_os, async_update_os,
@ -432,6 +430,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
async def update_info_data(_: datetime | None = None) -> None: async def update_info_data(_: datetime | None = None) -> None:
"""Update last available supervisor information.""" """Update last available supervisor information."""
supervisor_client = get_supervisor_client(hass)
try: try:
( (
@ -445,7 +444,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
) = await asyncio.gather( ) = await asyncio.gather(
create_eager_task(hassio.get_info()), create_eager_task(hassio.get_info()),
create_eager_task(hassio.get_host_info()), create_eager_task(hassio.get_host_info()),
create_eager_task(hassio.client.store.info()), create_eager_task(supervisor_client.store.info()),
create_eager_task(hassio.get_core_info()), create_eager_task(hassio.get_core_info()),
create_eager_task(hassio.get_supervisor_info()), create_eager_task(hassio.get_supervisor_info()),
create_eager_task(hassio.get_os_info()), create_eager_task(hassio.get_os_info()),

View File

@ -10,10 +10,12 @@ from functools import partial, wraps
import logging import logging
from typing import Any, Concatenate from typing import Any, Concatenate
from aiohasupervisor import SupervisorClient, SupervisorError from aiohasupervisor import SupervisorError
from aiohasupervisor.models import ( from aiohasupervisor.models import (
AddonsOptions,
AddonState as SupervisorAddonState, AddonState as SupervisorAddonState,
InstalledAddonComplete, InstalledAddonComplete,
StoreAddonUpdate,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -23,8 +25,6 @@ from .handler import (
HassioAPIError, HassioAPIError,
async_create_backup, async_create_backup,
async_get_addon_discovery_info, async_get_addon_discovery_info,
async_set_addon_options,
async_update_addon,
get_supervisor_client, get_supervisor_client,
) )
@ -36,10 +36,13 @@ type _ReturnFuncType[_T, **_P, _R] = Callable[
def api_error[_AddonManagerT: AddonManager, **_P, _R]( def api_error[_AddonManagerT: AddonManager, **_P, _R](
error_message: str, error_message: str,
*,
expected_error_type: type[HassioAPIError | SupervisorError] | None = None,
) -> Callable[ ) -> Callable[
[_FuncType[_AddonManagerT, _P, _R]], _ReturnFuncType[_AddonManagerT, _P, _R] [_FuncType[_AddonManagerT, _P, _R]], _ReturnFuncType[_AddonManagerT, _P, _R]
]: ]:
"""Handle HassioAPIError and raise a specific AddonError.""" """Handle HassioAPIError and raise a specific AddonError."""
error_type = expected_error_type or (HassioAPIError, SupervisorError)
def handle_hassio_api_error( def handle_hassio_api_error(
func: _FuncType[_AddonManagerT, _P, _R], func: _FuncType[_AddonManagerT, _P, _R],
@ -53,7 +56,7 @@ def api_error[_AddonManagerT: AddonManager, **_P, _R](
"""Wrap an add-on manager method.""" """Wrap an add-on manager method."""
try: try:
return_value = await func(self, *args, **kwargs) return_value = await func(self, *args, **kwargs)
except (HassioAPIError, SupervisorError) as err: except error_type as err:
raise AddonError( raise AddonError(
f"{error_message.format(addon_name=self.addon_name)}: {err}" f"{error_message.format(addon_name=self.addon_name)}: {err}"
) from err ) from err
@ -111,14 +114,7 @@ class AddonManager:
self._restart_task: asyncio.Task | None = None self._restart_task: asyncio.Task | None = None
self._start_task: asyncio.Task | None = None self._start_task: asyncio.Task | None = None
self._update_task: asyncio.Task | None = None self._update_task: asyncio.Task | None = None
self._client: SupervisorClient | None = None self._supervisor_client = get_supervisor_client(hass)
@property
def _supervisor_client(self) -> SupervisorClient:
"""Get supervisor client."""
if not self._client:
self._client = get_supervisor_client(self._hass)
return self._client
def task_in_progress(self) -> bool: def task_in_progress(self) -> bool:
"""Return True if any of the add-on tasks are in progress.""" """Return True if any of the add-on tasks are in progress."""
@ -145,7 +141,10 @@ class AddonManager:
discovery_info_config: dict = discovery_info["config"] discovery_info_config: dict = discovery_info["config"]
return discovery_info_config return discovery_info_config
@api_error("Failed to get the {addon_name} add-on info") @api_error(
"Failed to get the {addon_name} add-on info",
expected_error_type=SupervisorError,
)
async def async_get_addon_info(self) -> AddonInfo: async def async_get_addon_info(self) -> AddonInfo:
"""Return and cache manager add-on info.""" """Return and cache manager add-on info."""
addon_store_info = await self._supervisor_client.store.addon_info( addon_store_info = await self._supervisor_client.store.addon_info(
@ -187,19 +186,24 @@ class AddonManager:
return addon_state return addon_state
@api_error("Failed to set the {addon_name} add-on options") @api_error(
"Failed to set the {addon_name} add-on options",
expected_error_type=SupervisorError,
)
async def async_set_addon_options(self, config: dict) -> None: async def async_set_addon_options(self, config: dict) -> None:
"""Set manager add-on options.""" """Set manager add-on options."""
options = {"options": config} await self._supervisor_client.addons.addon_options(
await async_set_addon_options(self._hass, self.addon_slug, options) self.addon_slug, AddonsOptions(config=config)
)
def _check_addon_available(self, addon_info: AddonInfo) -> None: def _check_addon_available(self, addon_info: AddonInfo) -> None:
"""Check if the managed add-on is available.""" """Check if the managed add-on is available."""
if not addon_info.available: if not addon_info.available:
raise AddonError(f"{self.addon_name} add-on is not available") raise AddonError(f"{self.addon_name} add-on is not available")
@api_error("Failed to install the {addon_name} add-on") @api_error(
"Failed to install the {addon_name} add-on", expected_error_type=SupervisorError
)
async def async_install_addon(self) -> None: async def async_install_addon(self) -> None:
"""Install the managed add-on.""" """Install the managed add-on."""
addon_info = await self.async_get_addon_info() addon_info = await self.async_get_addon_info()
@ -208,7 +212,10 @@ class AddonManager:
await self._supervisor_client.store.install_addon(self.addon_slug) await self._supervisor_client.store.install_addon(self.addon_slug)
@api_error("Failed to uninstall the {addon_name} add-on") @api_error(
"Failed to uninstall the {addon_name} add-on",
expected_error_type=SupervisorError,
)
async def async_uninstall_addon(self) -> None: async def async_uninstall_addon(self) -> None:
"""Uninstall the managed add-on.""" """Uninstall the managed add-on."""
await self._supervisor_client.addons.uninstall_addon(self.addon_slug) await self._supervisor_client.addons.uninstall_addon(self.addon_slug)
@ -227,19 +234,27 @@ class AddonManager:
return return
await self.async_create_backup() await self.async_create_backup()
await async_update_addon(self._hass, self.addon_slug) await self._supervisor_client.store.update_addon(
self.addon_slug, StoreAddonUpdate(backup=False)
)
@api_error("Failed to start the {addon_name} add-on") @api_error(
"Failed to start the {addon_name} add-on", expected_error_type=SupervisorError
)
async def async_start_addon(self) -> None: async def async_start_addon(self) -> None:
"""Start the managed add-on.""" """Start the managed add-on."""
await self._supervisor_client.addons.start_addon(self.addon_slug) await self._supervisor_client.addons.start_addon(self.addon_slug)
@api_error("Failed to restart the {addon_name} add-on") @api_error(
"Failed to restart the {addon_name} add-on", expected_error_type=SupervisorError
)
async def async_restart_addon(self) -> None: async def async_restart_addon(self) -> None:
"""Restart the managed add-on.""" """Restart the managed add-on."""
await self._supervisor_client.addons.restart_addon(self.addon_slug) await self._supervisor_client.addons.restart_addon(self.addon_slug)
@api_error("Failed to stop the {addon_name} add-on") @api_error(
"Failed to stop the {addon_name} add-on", expected_error_type=SupervisorError
)
async def async_stop_addon(self) -> None: async def async_stop_addon(self) -> None:
"""Stop the managed add-on.""" """Stop the managed add-on."""
await self._supervisor_client.addons.stop_addon(self.addon_slug) await self._supervisor_client.addons.stop_addon(self.addon_slug)

View File

@ -56,7 +56,7 @@ from .const import (
SUPERVISOR_CONTAINER, SUPERVISOR_CONTAINER,
SupervisorEntityModel, SupervisorEntityModel,
) )
from .handler import HassIO, HassioAPIError from .handler import HassIO, HassioAPIError, get_supervisor_client
if TYPE_CHECKING: if TYPE_CHECKING:
from .issues import SupervisorIssues from .issues import SupervisorIssues
@ -318,6 +318,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict( self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
lambda: defaultdict(set) lambda: defaultdict(set)
) )
self._supervisor_client = get_supervisor_client(hass)
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library.""" """Update data via library."""
@ -502,17 +503,17 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]: async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Update single addon stats.""" """Update single addon stats."""
try: try:
stats = await self.hassio.get_addon_stats(slug) stats = await self._supervisor_client.addons.addon_stats(slug)
except HassioAPIError as err: except SupervisorError as err:
_LOGGER.warning("Could not fetch stats for %s: %s", slug, err) _LOGGER.warning("Could not fetch stats for %s: %s", slug, err)
return (slug, None) return (slug, None)
return (slug, stats) return (slug, stats.to_dict())
async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]: async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]:
"""Return the changelog for an add-on.""" """Return the changelog for an add-on."""
try: try:
changelog = await self.hassio.get_addon_changelog(slug) changelog = await self._supervisor_client.store.addon_changelog(slug)
except HassioAPIError as err: except SupervisorError as err:
_LOGGER.warning("Could not fetch changelog for %s: %s", slug, err) _LOGGER.warning("Could not fetch changelog for %s: %s", slug, err)
return (slug, None) return (slug, None)
return (slug, changelog) return (slug, changelog)
@ -520,7 +521,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]: async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Return the info for an add-on.""" """Return the info for an add-on."""
try: try:
info = await self.hassio.client.addons.addon_info(slug) info = await self._supervisor_client.addons.addon_info(slug)
except SupervisorError as err: except SupervisorError as err:
_LOGGER.warning("Could not fetch info for %s: %s", slug, err) _LOGGER.warning("Could not fetch info for %s: %s", slug, err)
return (slug, None) return (slug, None)

View File

@ -7,6 +7,7 @@ from dataclasses import dataclass
import logging import logging
from typing import Any from typing import Any
from aiohasupervisor import SupervisorError
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import HTTPServiceUnavailable from aiohttp.web_exceptions import HTTPServiceUnavailable
@ -19,7 +20,7 @@ from homeassistant.helpers import discovery_flow
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID, DOMAIN from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID, DOMAIN
from .handler import HassIO, HassioAPIError from .handler import HassIO, HassioAPIError, get_supervisor_client
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -88,6 +89,7 @@ class HassIODiscovery(HomeAssistantView):
"""Initialize WebView.""" """Initialize WebView."""
self.hass = hass self.hass = hass
self.hassio = hassio self.hassio = hassio
self._supervisor_client = get_supervisor_client(hass)
async def post(self, request: web.Request, uuid: str) -> web.Response: async def post(self, request: web.Request, uuid: str) -> web.Response:
"""Handle new discovery requests.""" """Handle new discovery requests."""
@ -126,8 +128,8 @@ class HassIODiscovery(HomeAssistantView):
# Read additional Add-on info # Read additional Add-on info
try: try:
addon_info = await self.hassio.client.addons.addon_info(slug) addon_info = await self._supervisor_client.addons.addon_info(slug)
except HassioAPIError as err: except SupervisorError as err:
_LOGGER.error("Can't read add-on info: %s", err) _LOGGER.error("Can't read add-on info: %s", err)
return return

View File

@ -21,12 +21,15 @@ from homeassistant.components.http import (
) )
from homeassistant.const import SERVER_PORT from homeassistant.const import SERVER_PORT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.singleton import singleton
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from .const import ATTR_DISCOVERY, ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE from .const import ATTR_DISCOVERY, ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
KEY_SUPERVISOR_CLIENT = "supervisor_client"
class HassioAPIError(RuntimeError): class HassioAPIError(RuntimeError):
"""Return if a API trow a error.""" """Return if a API trow a error."""
@ -73,40 +76,6 @@ async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> bo
return await hassio.update_diagnostics(diagnostics) return await hassio.update_diagnostics(diagnostics)
@bind_hass
@api_data
async def async_update_addon(
hass: HomeAssistant,
slug: str,
backup: bool = False,
) -> dict:
"""Update add-on.
The caller of the function should handle HassioAPIError.
"""
hassio: HassIO = hass.data[DOMAIN]
command = f"/addons/{slug}/update"
return await hassio.send_command(
command,
payload={"backup": backup},
timeout=None,
)
@bind_hass
@api_data
async def async_set_addon_options(
hass: HomeAssistant, slug: str, options: dict
) -> dict:
"""Set add-on options.
The caller of the function should handle HassioAPIError.
"""
hassio: HassIO = hass.data[DOMAIN]
command = f"/addons/{slug}/options"
return await hassio.send_command(command, payload=options)
@bind_hass @bind_hass
async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None: async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None:
"""Return discovery data for an add-on.""" """Return discovery data for an add-on."""
@ -253,14 +222,11 @@ class HassIO:
self._ip = ip self._ip = ip
base_url = f"http://{ip}" base_url = f"http://{ip}"
self._base_url = URL(base_url) self._base_url = URL(base_url)
self._client = SupervisorClient(
base_url, os.environ.get("SUPERVISOR_TOKEN", ""), session=websession
)
@property @property
def client(self) -> SupervisorClient: def base_url(self) -> URL:
"""Return aiohasupervisor client.""" """Return base url for Supervisor."""
return self._client return self._base_url
@_api_bool @_api_bool
def is_connected(self) -> Coroutine: def is_connected(self) -> Coroutine:
@ -326,14 +292,6 @@ class HassIO:
""" """
return self.send_command("/core/stats", method="get") return self.send_command("/core/stats", method="get")
@api_data
def get_addon_stats(self, addon: str) -> Coroutine:
"""Return stats for an Add-on.
This method returns a coroutine.
"""
return self.send_command(f"/addons/{addon}/stats", method="get")
@api_data @api_data
def get_supervisor_stats(self) -> Coroutine: def get_supervisor_stats(self) -> Coroutine:
"""Return stats for the supervisor. """Return stats for the supervisor.
@ -342,15 +300,6 @@ class HassIO:
""" """
return self.send_command("/supervisor/stats", method="get") return self.send_command("/supervisor/stats", method="get")
def get_addon_changelog(self, addon: str) -> Coroutine:
"""Return changelog for an Add-on.
This method returns a coroutine.
"""
return self.send_command(
f"/addons/{addon}/changelog", method="get", return_text=True
)
@api_data @api_data
def get_ingress_panels(self) -> Coroutine: def get_ingress_panels(self) -> Coroutine:
"""Return data for Add-on ingress panels. """Return data for Add-on ingress panels.
@ -531,7 +480,12 @@ class HassIO:
raise HassioAPIError raise HassioAPIError
@singleton(KEY_SUPERVISOR_CLIENT)
def get_supervisor_client(hass: HomeAssistant) -> SupervisorClient: def get_supervisor_client(hass: HomeAssistant) -> SupervisorClient:
"""Return supervisor client.""" """Return supervisor client."""
hassio: HassIO = hass.data[DOMAIN] hassio: HassIO = hass.data[DOMAIN]
return hassio.client return SupervisorClient(
hassio.base_url,
os.environ.get("SUPERVISOR_TOKEN", ""),
session=hassio.websession,
)

View File

@ -4,6 +4,8 @@ from __future__ import annotations
from typing import Any from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import StoreAddonUpdate
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.components.update import ( from homeassistant.components.update import (
@ -15,6 +17,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ICON, ATTR_NAME from homeassistant.const import ATTR_ICON, ATTR_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
@ -28,6 +31,7 @@ from .const import (
DATA_KEY_OS, DATA_KEY_OS,
DATA_KEY_SUPERVISOR, DATA_KEY_SUPERVISOR,
) )
from .coordinator import HassioDataUpdateCoordinator
from .entity import ( from .entity import (
HassioAddonEntity, HassioAddonEntity,
HassioCoreEntity, HassioCoreEntity,
@ -36,10 +40,10 @@ from .entity import (
) )
from .handler import ( from .handler import (
HassioAPIError, HassioAPIError,
async_update_addon,
async_update_core, async_update_core,
async_update_os, async_update_os,
async_update_supervisor, async_update_supervisor,
get_supervisor_client,
) )
ENTITY_DESCRIPTION = UpdateEntityDescription( ENTITY_DESCRIPTION = UpdateEntityDescription(
@ -96,6 +100,16 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
| UpdateEntityFeature.RELEASE_NOTES | UpdateEntityFeature.RELEASE_NOTES
) )
def __init__(
self,
coordinator: HassioDataUpdateCoordinator,
entity_description: EntityDescription,
addon: dict[str, Any],
) -> None:
"""Initialize object."""
super().__init__(coordinator, entity_description, addon)
self._supervisor_client = get_supervisor_client(self.hass)
@property @property
def _addon_data(self) -> dict: def _addon_data(self) -> dict:
"""Return the add-on data.""" """Return the add-on data."""
@ -165,8 +179,10 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
) -> None: ) -> None:
"""Install an update.""" """Install an update."""
try: try:
await async_update_addon(self.hass, slug=self._addon_slug, backup=backup) await self._supervisor_client.store.update_addon(
except HassioAPIError as err: self._addon_slug, StoreAddonUpdate(backup=backup)
)
except SupervisorError as err:
raise HomeAssistantError(f"Error updating {self.title}: {err}") from err raise HomeAssistantError(f"Error updating {self.title}: {err}") from err
await self.coordinator.force_info_update_supervisor() await self.coordinator.force_info_update_supervisor()

View File

@ -6,7 +6,7 @@ from collections.abc import Callable, Generator
from importlib.util import find_spec from importlib.util import find_spec
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from aiohasupervisor.models import Repository, StoreAddon, StoreInfo from aiohasupervisor.models import Repository, StoreAddon, StoreInfo
import pytest import pytest
@ -194,7 +194,9 @@ def mock_legacy_device_tracker_setup() -> Callable[[HomeAssistant, MockScanner],
@pytest.fixture(name="addon_manager") @pytest.fixture(name="addon_manager")
def addon_manager_fixture(hass: HomeAssistant) -> AddonManager: def addon_manager_fixture(
hass: HomeAssistant, supervisor_client: AsyncMock
) -> AddonManager:
"""Return an AddonManager instance.""" """Return an AddonManager instance."""
# pylint: disable-next=import-outside-toplevel # pylint: disable-next=import-outside-toplevel
from .hassio.common import mock_addon_manager from .hassio.common import mock_addon_manager
@ -363,10 +365,7 @@ def stop_addon_fixture(supervisor_client: AsyncMock) -> AsyncMock:
@pytest.fixture(name="addon_options") @pytest.fixture(name="addon_options")
def addon_options_fixture(addon_info: AsyncMock) -> dict[str, Any]: def addon_options_fixture(addon_info: AsyncMock) -> dict[str, Any]:
"""Mock add-on options.""" """Mock add-on options."""
# pylint: disable-next=import-outside-toplevel return addon_info.return_value.options
from .hassio.common import mock_addon_options
return mock_addon_options(addon_info)
@pytest.fixture(name="set_addon_options_side_effect") @pytest.fixture(name="set_addon_options_side_effect")
@ -382,13 +381,12 @@ def set_addon_options_side_effect_fixture(
@pytest.fixture(name="set_addon_options") @pytest.fixture(name="set_addon_options")
def set_addon_options_fixture( def set_addon_options_fixture(
supervisor_client: AsyncMock,
set_addon_options_side_effect: Any | None, set_addon_options_side_effect: Any | None,
) -> Generator[AsyncMock]: ) -> AsyncMock:
"""Mock set add-on options.""" """Mock set add-on options."""
# pylint: disable-next=import-outside-toplevel supervisor_client.addons.addon_options.side_effect = set_addon_options_side_effect
from .hassio.common import mock_set_addon_options return supervisor_client.addons.addon_options
yield from mock_set_addon_options(set_addon_options_side_effect)
@pytest.fixture(name="uninstall_addon") @pytest.fixture(name="uninstall_addon")
@ -407,12 +405,9 @@ def create_backup_fixture() -> Generator[AsyncMock]:
@pytest.fixture(name="update_addon") @pytest.fixture(name="update_addon")
def update_addon_fixture() -> Generator[AsyncMock]: def update_addon_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock update add-on.""" """Mock update add-on."""
# pylint: disable-next=import-outside-toplevel return supervisor_client.store.update_addon
from .hassio.common import mock_update_addon
yield from mock_update_addon()
@pytest.fixture(name="store_addons") @pytest.fixture(name="store_addons")
@ -440,6 +435,22 @@ def store_info_fixture(
return supervisor_client.store.info return supervisor_client.store.info
@pytest.fixture(name="addon_stats")
def addon_stats_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock addon stats info."""
# pylint: disable-next=import-outside-toplevel
from .hassio.common import mock_addon_stats
return mock_addon_stats(supervisor_client)
@pytest.fixture(name="addon_changelog")
def addon_changelog_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock addon changelog."""
supervisor_client.store.addon_changelog.return_value = ""
return supervisor_client.store.addon_changelog
@pytest.fixture(name="supervisor_client") @pytest.fixture(name="supervisor_client")
def supervisor_client() -> Generator[AsyncMock]: def supervisor_client() -> Generator[AsyncMock]:
"""Mock the supervisor client.""" """Mock the supervisor client."""
@ -459,8 +470,20 @@ def supervisor_client() -> Generator[AsyncMock]:
return_value=supervisor_client, return_value=supervisor_client,
), ),
patch( patch(
"homeassistant.components.hassio.handler.HassIO.client", "homeassistant.components.hassio.discovery.get_supervisor_client",
new=PropertyMock(return_value=supervisor_client), return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.coordinator.get_supervisor_client",
return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.update.get_supervisor_client",
return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.get_supervisor_client",
return_value=supervisor_client,
), ),
): ):
yield supervisor_client yield supervisor_client

View File

@ -10,6 +10,8 @@ from typing import Any
from unittest.mock import DEFAULT, AsyncMock, Mock, patch from unittest.mock import DEFAULT, AsyncMock, Mock, patch
from aiohasupervisor.models import ( from aiohasupervisor.models import (
AddonsOptions,
AddonsStats,
AddonStage, AddonStage,
InstalledAddonComplete, InstalledAddonComplete,
Repository, Repository,
@ -23,6 +25,7 @@ from homeassistant.core import HomeAssistant
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
INSTALLED_ADDON_FIELDS = [field.name for field in fields(InstalledAddonComplete)] INSTALLED_ADDON_FIELDS = [field.name for field in fields(InstalledAddonComplete)]
STORE_ADDON_FIELDS = [field.name for field in fields(StoreAddonComplete)] STORE_ADDON_FIELDS = [field.name for field in fields(StoreAddonComplete)]
ADDONS_STATS_FIELDS = [field.name for field in fields(AddonsStats)]
MOCK_STORE_ADDONS = [ MOCK_STORE_ADDONS = [
StoreAddon( StoreAddon(
@ -202,32 +205,16 @@ def mock_start_addon_side_effect(
return start_addon return start_addon
def mock_addon_options(addon_info: AsyncMock) -> dict[str, Any]:
"""Mock add-on options."""
return addon_info.return_value.options
def mock_set_addon_options_side_effect(addon_options: dict[str, Any]) -> Any | None: def mock_set_addon_options_side_effect(addon_options: dict[str, Any]) -> Any | None:
"""Return the set add-on options side effect.""" """Return the set add-on options side effect."""
async def set_addon_options(hass: HomeAssistant, slug: str, options: dict) -> None: async def set_addon_options(slug: str, options: AddonsOptions) -> None:
"""Mock set add-on options.""" """Mock set add-on options."""
addon_options.update(options["options"]) addon_options.update(options.config)
return set_addon_options return set_addon_options
def mock_set_addon_options(
set_addon_options_side_effect: Any | None,
) -> Generator[AsyncMock]:
"""Mock set add-on options."""
with patch(
"homeassistant.components.hassio.addon_manager.async_set_addon_options",
side_effect=set_addon_options_side_effect,
) as set_options:
yield set_options
def mock_create_backup() -> Generator[AsyncMock]: def mock_create_backup() -> Generator[AsyncMock]:
"""Mock create backup.""" """Mock create backup."""
with patch( with patch(
@ -236,9 +223,21 @@ def mock_create_backup() -> Generator[AsyncMock]:
yield create_backup yield create_backup
def mock_update_addon() -> Generator[AsyncMock]: def mock_addon_stats(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock update add-on.""" """Mock addon stats."""
with patch( supervisor_client.addons.addon_stats.return_value = addon_stats = Mock(
"homeassistant.components.hassio.addon_manager.async_update_addon" spec=AddonsStats,
) as update_addon: cpu_percent=0.99,
yield update_addon memory_usage=182611968,
memory_limit=3977146368,
memory_percent=4.59,
network_rx=362570232,
network_tx=82374138,
blk_read=46010945536,
blk_write=15051526144,
)
addon_stats.to_dict = MethodType(
lambda self: mock_to_dict(self, ADDONS_STATS_FIELDS),
addon_stats,
)
return supervisor_client.addons.addon_stats

View File

@ -5,7 +5,7 @@ import os
import re import re
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aiohasupervisor.models import AddonState from aiohasupervisor.models import AddonsStats, AddonState
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
import pytest import pytest
@ -55,6 +55,7 @@ def hassio_stubs(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
) -> RefreshToken: ) -> RefreshToken:
"""Create mock hassio http client.""" """Create mock hassio http client."""
with ( with (
@ -133,7 +134,9 @@ def all_setup_requests(
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
request: pytest.FixtureRequest, request: pytest.FixtureRequest,
addon_installed: AsyncMock, addon_installed: AsyncMock,
store_info, store_info: AsyncMock,
addon_changelog: AsyncMock,
addon_stats: AsyncMock,
) -> None: ) -> None:
"""Mock all setup requests.""" """Mock all setup requests."""
include_addons = hasattr(request, "param") and request.param.get( include_addons = hasattr(request, "param") and request.param.get(
@ -249,8 +252,6 @@ def all_setup_requests(
addon_installed.side_effect = mock_addon_info addon_installed.side_effect = mock_addon_info
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/core/stats", "http://127.0.0.1/core/stats",
json={ json={
@ -283,38 +284,32 @@ def all_setup_requests(
}, },
}, },
) )
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats", async def mock_addon_stats(addon: str) -> AddonsStats:
json={ """Mock addon stats for test and test2."""
"result": "ok", if addon == "test2":
"data": { return AddonsStats(
"cpu_percent": 0.99, cpu_percent=0.8,
"memory_usage": 182611968, memory_usage=51941376,
"memory_limit": 3977146368, memory_limit=3977146368,
"memory_percent": 4.59, memory_percent=1.31,
"network_rx": 362570232, network_rx=31338284,
"network_tx": 82374138, network_tx=15692900,
"blk_read": 46010945536, blk_read=740077568,
"blk_write": 15051526144, blk_write=6004736,
},
},
) )
aioclient_mock.get( return AddonsStats(
"http://127.0.0.1/addons/test2/stats", cpu_percent=0.99,
json={ memory_usage=182611968,
"result": "ok", memory_limit=3977146368,
"data": { memory_percent=4.59,
"cpu_percent": 0.8, network_rx=362570232,
"memory_usage": 51941376, network_tx=82374138,
"memory_limit": 3977146368, blk_read=46010945536,
"memory_percent": 1.31, blk_write=15051526144,
"network_rx": 31338284,
"network_tx": 15692900,
"blk_read": 740077568,
"blk_write": 6004736,
},
},
) )
addon_stats.side_effect = mock_addon_stats
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/network/info", "http://127.0.0.1/network/info",
json={ json={

View File

@ -7,6 +7,7 @@ from typing import Any
from unittest.mock import AsyncMock, call from unittest.mock import AsyncMock, call
from aiohasupervisor import SupervisorError from aiohasupervisor import SupervisorError
from aiohasupervisor.models import AddonsOptions
import pytest import pytest
from homeassistant.components.hassio.addon_manager import ( from homeassistant.components.hassio.addon_manager import (
@ -137,7 +138,7 @@ async def test_get_addon_info(
"addon_store_info_error", "addon_store_info_error",
"addon_store_info_calls", "addon_store_info_calls",
), ),
[(SupervisorError("Boom"), 1, None, 1), (None, 0, HassioAPIError("Boom"), 1)], [(SupervisorError("Boom"), 1, None, 1), (None, 0, SupervisorError("Boom"), 1)],
) )
async def test_get_addon_info_error( async def test_get_addon_info_error(
addon_manager: AddonManager, addon_manager: AddonManager,
@ -170,7 +171,7 @@ async def test_set_addon_options(
assert set_addon_options.call_count == 1 assert set_addon_options.call_count == 1
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "test_addon", {"options": {"test_key": "test"}} "test_addon", AddonsOptions(config={"test_key": "test"})
) )
@ -178,7 +179,7 @@ async def test_set_addon_options_error(
hass: HomeAssistant, addon_manager: AddonManager, set_addon_options: AsyncMock hass: HomeAssistant, addon_manager: AddonManager, set_addon_options: AsyncMock
) -> None: ) -> None:
"""Test set addon options raises error.""" """Test set addon options raises error."""
set_addon_options.side_effect = HassioAPIError("Boom") set_addon_options.side_effect = SupervisorError("Boom")
with pytest.raises(AddonError) as err: with pytest.raises(AddonError) as err:
await addon_manager.async_set_addon_options({"test_key": "test"}) await addon_manager.async_set_addon_options({"test_key": "test"})
@ -187,7 +188,7 @@ async def test_set_addon_options_error(
assert set_addon_options.call_count == 1 assert set_addon_options.call_count == 1
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "test_addon", {"options": {"test_key": "test"}} "test_addon", AddonsOptions(config={"test_key": "test"})
) )
@ -215,7 +216,7 @@ async def test_install_addon_error(
"""Test install addon raises error.""" """Test install addon raises error."""
addon_store_info.return_value.available = True addon_store_info.return_value.available = True
addon_info.return_value.available = True addon_info.return_value.available = True
install_addon.side_effect = HassioAPIError("Boom") install_addon.side_effect = SupervisorError("Boom")
with pytest.raises(AddonError) as err: with pytest.raises(AddonError) as err:
await addon_manager.async_install_addon() await addon_manager.async_install_addon()
@ -266,7 +267,7 @@ async def test_schedule_install_addon_error(
install_addon: AsyncMock, install_addon: AsyncMock,
) -> None: ) -> None:
"""Test schedule install addon raises error.""" """Test schedule install addon raises error."""
install_addon.side_effect = HassioAPIError("Boom") install_addon.side_effect = SupervisorError("Boom")
with pytest.raises(AddonError) as err: with pytest.raises(AddonError) as err:
await addon_manager.async_schedule_install_addon() await addon_manager.async_schedule_install_addon()
@ -283,7 +284,7 @@ async def test_schedule_install_addon_logs_error(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test schedule install addon logs error.""" """Test schedule install addon logs error."""
install_addon.side_effect = HassioAPIError("Boom") install_addon.side_effect = SupervisorError("Boom")
await addon_manager.async_schedule_install_addon(catch_error=True) await addon_manager.async_schedule_install_addon(catch_error=True)
@ -541,7 +542,7 @@ async def test_update_addon_error(
) -> None: ) -> None:
"""Test update addon raises error.""" """Test update addon raises error."""
addon_info.return_value.update_available = True addon_info.return_value.update_available = True
update_addon.side_effect = HassioAPIError("Boom") update_addon.side_effect = SupervisorError("Boom")
with pytest.raises(AddonError) as err: with pytest.raises(AddonError) as err:
await addon_manager.async_update_addon() await addon_manager.async_update_addon()
@ -620,7 +621,7 @@ async def test_schedule_update_addon(
( (
None, None,
1, 1,
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
"Failed to update the Test add-on: Boom", "Failed to update the Test add-on: Boom",
), ),
@ -670,7 +671,7 @@ async def test_schedule_update_addon_error(
( (
None, None,
1, 1,
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
"Failed to update the Test add-on: Boom", "Failed to update the Test add-on: Boom",
), ),
@ -790,7 +791,7 @@ async def test_schedule_install_setup_addon(
), ),
[ [
( (
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
None, None,
0, 0,
@ -801,7 +802,7 @@ async def test_schedule_install_setup_addon(
( (
None, None,
1, 1,
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
None, None,
0, 0,
@ -859,7 +860,7 @@ async def test_schedule_install_setup_addon_error(
), ),
[ [
( (
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
None, None,
0, 0,
@ -870,7 +871,7 @@ async def test_schedule_install_setup_addon_error(
( (
None, None,
1, 1,
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
None, None,
0, 0,
@ -956,7 +957,7 @@ async def test_schedule_setup_addon(
), ),
[ [
( (
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
None, None,
0, 0,
@ -1005,7 +1006,7 @@ async def test_schedule_setup_addon_error(
), ),
[ [
( (
HassioAPIError("Boom"), SupervisorError("Boom"),
1, 1,
None, None,
0, 0,

View File

@ -19,7 +19,13 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -> None: def mock_all(
aioclient_mock: AiohttpClientMocker,
addon_installed: AsyncMock,
store_info: AsyncMock,
addon_changelog: AsyncMock,
addon_stats: AsyncMock,
) -> None:
"""Mock all setup requests.""" """Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
@ -100,22 +106,6 @@ def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -
}, },
}, },
) )
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats",
json={
"result": "ok",
"data": {
"cpu_percent": 0.99,
"memory_usage": 182611968,
"memory_limit": 3977146368,
"memory_percent": 4.59,
"network_rx": 362570232,
"network_tx": 82374138,
"blk_read": 46010945536,
"blk_write": 15051526144,
},
},
)
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/core/stats", "http://127.0.0.1/core/stats",
json={ json={
@ -148,8 +138,6 @@ def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -
}, },
}, },
) )
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
) )

View File

@ -1,7 +1,7 @@
"""Test Supervisor diagnostics.""" """Test Supervisor diagnostics."""
import os import os
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@ -18,7 +18,13 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -> None: def mock_all(
aioclient_mock: AiohttpClientMocker,
addon_installed: AsyncMock,
store_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
) -> None:
"""Mock all setup requests.""" """Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
@ -103,22 +109,6 @@ def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -
}, },
}, },
) )
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats",
json={
"result": "ok",
"data": {
"cpu_percent": 0.99,
"memory_usage": 182611968,
"memory_limit": 3977146368,
"memory_percent": 4.59,
"network_rx": 362570232,
"network_tx": 82374138,
"blk_read": 46010945536,
"blk_write": 15051526144,
},
},
)
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/core/stats", "http://127.0.0.1/core/stats",
json={ json={
@ -151,8 +141,6 @@ def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -
}, },
}, },
) )
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
) )

View File

@ -201,20 +201,6 @@ async def test_api_homeassistant_restart(
assert aioclient_mock.call_count == 1 assert aioclient_mock.call_count == 1
async def test_api_addon_stats(
hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test setup with API Add-on stats."""
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats",
json={"result": "ok", "data": {"memory_percent": 0.01}},
)
data = await hassio_handler.get_addon_stats("test")
assert data["memory_percent"] == 0.01
assert aioclient_mock.call_count == 1
async def test_api_core_stats( async def test_api_core_stats(
hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None:

View File

@ -5,6 +5,7 @@ import os
from typing import Any from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohasupervisor.models import AddonsStats
import pytest import pytest
from voluptuous import Invalid from voluptuous import Invalid
@ -52,7 +53,12 @@ def os_info(extra_os_info):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_all( def mock_all(
aioclient_mock: AiohttpClientMocker, os_info, store_info, addon_info aioclient_mock: AiohttpClientMocker,
os_info: AsyncMock,
store_info: AsyncMock,
addon_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
) -> None: ) -> None:
"""Mock all setup requests.""" """Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@ -156,64 +162,38 @@ def mock_all(
}, },
}, },
) )
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats", async def mock_addon_stats(addon: str) -> AddonsStats:
json={ """Mock addon stats for test and test2."""
"result": "ok", if addon in {"test2", "test3"}:
"data": { return AddonsStats(
"cpu_percent": 0.99, cpu_percent=0.8,
"memory_usage": 182611968, memory_usage=51941376,
"memory_limit": 3977146368, memory_limit=3977146368,
"memory_percent": 4.59, memory_percent=1.31,
"network_rx": 362570232, network_rx=31338284,
"network_tx": 82374138, network_tx=15692900,
"blk_read": 46010945536, blk_read=740077568,
"blk_write": 15051526144, blk_write=6004736,
},
},
) )
aioclient_mock.get( return AddonsStats(
"http://127.0.0.1/addons/test2/stats", cpu_percent=0.99,
json={ memory_usage=182611968,
"result": "ok", memory_limit=3977146368,
"data": { memory_percent=4.59,
"cpu_percent": 0.8, network_rx=362570232,
"memory_usage": 51941376, network_tx=82374138,
"memory_limit": 3977146368, blk_read=46010945536,
"memory_percent": 1.31, blk_write=15051526144,
"network_rx": 31338284,
"network_tx": 15692900,
"blk_read": 740077568,
"blk_write": 6004736,
},
},
)
aioclient_mock.get(
"http://127.0.0.1/addons/test3/stats",
json={
"result": "ok",
"data": {
"cpu_percent": 0.8,
"memory_usage": 51941376,
"memory_limit": 3977146368,
"memory_percent": 1.31,
"network_rx": 31338284,
"network_tx": 15692900,
"blk_read": 740077568,
"blk_write": 6004736,
},
},
)
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get(
"http://127.0.0.1/addons/test/info",
json={"result": "ok", "data": {"auto_update": True}},
)
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get(
"http://127.0.0.1/addons/test2/info",
json={"result": "ok", "data": {"auto_update": False}},
) )
addon_stats.side_effect = mock_addon_stats
def mock_addon_info(slug: str):
addon_info.return_value.auto_update = slug == "test"
return addon_info.return_value
addon_info.side_effect = mock_addon_info
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
) )

View File

@ -4,15 +4,12 @@ from datetime import timedelta
import os import os
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohasupervisor import SupervisorError
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.hassio import ( from homeassistant.components.hassio import DOMAIN, HASSIO_UPDATE_INTERVAL
DOMAIN,
HASSIO_UPDATE_INTERVAL,
HassioAPIError,
)
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
@ -34,38 +31,11 @@ def mock_all(
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
addon_installed: AsyncMock, addon_installed: AsyncMock,
store_info: AsyncMock, store_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
) -> None: ) -> None:
"""Mock all setup requests.""" """Mock all setup requests."""
_install_default_mocks(aioclient_mock) _install_default_mocks(aioclient_mock)
_install_test_addon_stats_mock(aioclient_mock)
def _install_test_addon_stats_mock(aioclient_mock: AiohttpClientMocker):
"""Install mock to provide valid stats for the test addon."""
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats",
json={
"result": "ok",
"data": {
"cpu_percent": 0.99,
"memory_usage": 182611968,
"memory_limit": 3977146368,
"memory_percent": 4.59,
"network_rx": 362570232,
"network_tx": 82374138,
"blk_read": 46010945536,
"blk_write": 15051526144,
},
},
)
def _install_test_addon_stats_failure_mock(aioclient_mock: AiohttpClientMocker):
"""Install mocks to raise an exception when fetching stats for the test addon."""
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats",
exc=HassioAPIError,
)
def _install_default_mocks(aioclient_mock: AiohttpClientMocker): def _install_default_mocks(aioclient_mock: AiohttpClientMocker):
@ -174,8 +144,6 @@ def _install_default_mocks(aioclient_mock: AiohttpClientMocker):
}, },
}, },
) )
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
) )
@ -285,6 +253,7 @@ async def test_stats_addon_sensor(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
addon_stats: AsyncMock,
) -> None: ) -> None:
"""Test stats addons sensor.""" """Test stats addons sensor."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
@ -302,7 +271,7 @@ async def test_stats_addon_sensor(
aioclient_mock.clear_requests() aioclient_mock.clear_requests()
_install_default_mocks(aioclient_mock) _install_default_mocks(aioclient_mock)
_install_test_addon_stats_failure_mock(aioclient_mock) addon_stats.side_effect = SupervisorError
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
@ -312,7 +281,7 @@ async def test_stats_addon_sensor(
aioclient_mock.clear_requests() aioclient_mock.clear_requests()
_install_default_mocks(aioclient_mock) _install_default_mocks(aioclient_mock)
_install_test_addon_stats_mock(aioclient_mock) addon_stats.side_effect = None
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
@ -345,7 +314,7 @@ async def test_stats_addon_sensor(
aioclient_mock.clear_requests() aioclient_mock.clear_requests()
_install_default_mocks(aioclient_mock) _install_default_mocks(aioclient_mock)
_install_test_addon_stats_failure_mock(aioclient_mock) addon_stats.side_effect = SupervisorError
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)

View File

@ -4,7 +4,8 @@ from datetime import timedelta
import os import os
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohasupervisor import SupervisorBadRequestError from aiohasupervisor import SupervisorBadRequestError, SupervisorError
from aiohasupervisor.models import StoreAddonUpdate
import pytest import pytest
from homeassistant.components.hassio import DOMAIN, HassioAPIError from homeassistant.components.hassio import DOMAIN, HassioAPIError
@ -22,7 +23,13 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -> None: def mock_all(
aioclient_mock: AiohttpClientMocker,
addon_installed: AsyncMock,
store_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
) -> None:
"""Mock all setup requests.""" """Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
@ -108,22 +115,6 @@ def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -
}, },
}, },
) )
aioclient_mock.get(
"http://127.0.0.1/addons/test/stats",
json={
"result": "ok",
"data": {
"cpu_percent": 0.99,
"memory_usage": 182611968,
"memory_limit": 3977146368,
"memory_percent": 4.59,
"network_rx": 362570232,
"network_tx": 82374138,
"blk_read": 46010945536,
"blk_write": 15051526144,
},
},
)
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/core/stats", "http://127.0.0.1/core/stats",
json={ json={
@ -156,8 +147,6 @@ def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed, store_info) -
}, },
}, },
) )
aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="")
aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="")
aioclient_mock.get( aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
) )
@ -227,9 +216,7 @@ async def test_update_entities(
assert state.attributes["auto_update"] is auto_update assert state.attributes["auto_update"] is auto_update
async def test_update_addon( async def test_update_addon(hass: HomeAssistant, update_addon: AsyncMock) -> None:
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test updating addon update entity.""" """Test updating addon update entity."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -243,17 +230,13 @@ async def test_update_addon(
assert result assert result
await hass.async_block_till_done() await hass.async_block_till_done()
aioclient_mock.post(
"http://127.0.0.1/addons/test/update",
json={"result": "ok", "data": {}},
)
await hass.services.async_call( await hass.services.async_call(
"update", "update",
"install", "install",
{"entity_id": "update.test_update"}, {"entity_id": "update.test_update"},
blocking=True, blocking=True,
) )
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
async def test_update_os( async def test_update_os(
@ -344,7 +327,8 @@ async def test_update_supervisor(
async def test_update_addon_with_error( async def test_update_addon_with_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant,
update_addon: AsyncMock,
) -> None: ) -> None:
"""Test updating addon update entity with error.""" """Test updating addon update entity with error."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
@ -358,11 +342,7 @@ async def test_update_addon_with_error(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
aioclient_mock.post( update_addon.side_effect = SupervisorError
"http://127.0.0.1/addons/test/update",
exc=HassioAPIError,
)
with pytest.raises(HomeAssistantError, match=r"^Error updating test:"): with pytest.raises(HomeAssistantError, match=r"^Error updating test:"):
assert not await hass.services.async_call( assert not await hass.services.async_call(
"update", "update",
@ -610,19 +590,15 @@ async def test_setting_up_core_update_when_addon_fails(
hass: HomeAssistant, hass: HomeAssistant,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
addon_installed: AsyncMock, addon_installed: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
) -> None: ) -> None:
"""Test setting up core update when single addon fails.""" """Test setting up core update when single addon fails."""
addon_installed.side_effect = SupervisorBadRequestError("Addon Test does not exist") addon_installed.side_effect = SupervisorBadRequestError("Addon Test does not exist")
addon_stats.side_effect = SupervisorBadRequestError("add-on is not running")
addon_changelog.side_effect = SupervisorBadRequestError("add-on is not running")
with ( with (
patch.dict(os.environ, MOCK_ENVIRON), patch.dict(os.environ, MOCK_ENVIRON),
patch(
"homeassistant.components.hassio.HassIO.get_addon_stats",
side_effect=HassioAPIError("add-on is not running"),
),
patch(
"homeassistant.components.hassio.HassIO.get_addon_changelog",
side_effect=HassioAPIError("add-on is not running"),
),
): ):
result = await async_setup_component( result = await async_setup_component(
hass, hass,

View File

@ -7,15 +7,10 @@ from typing import Any
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aiohasupervisor import SupervisorError from aiohasupervisor import SupervisorError
from aiohasupervisor.models import AddonsOptions
import pytest import pytest
from homeassistant.components.hassio import ( from homeassistant.components.hassio import AddonError, AddonInfo, AddonState, HassIO
AddonError,
AddonInfo,
AddonState,
HassIO,
HassioAPIError,
)
from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow
@ -38,6 +33,11 @@ TEST_DOMAIN = "test"
TEST_DOMAIN_2 = "test_2" TEST_DOMAIN_2 = "test_2"
@pytest.fixture(autouse=True)
def mock_supervisor_client(supervisor_client: AsyncMock) -> None:
"""Mock supervisor client."""
class FakeConfigFlow(ConfigFlow): class FakeConfigFlow(ConfigFlow):
"""Handle a config flow for the silabs multiprotocol add-on.""" """Handle a config flow for the silabs multiprotocol add-on."""
@ -253,16 +253,15 @@ async def test_option_flow_install_multi_pan_addon(
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"
set_addon_options.assert_called_once_with( set_addon_options.assert_called_once_with(
hass,
"core_silabs_multiprotocol", "core_silabs_multiprotocol",
{ AddonsOptions(
"options": { config={
"autoflash_firmware": True, "autoflash_firmware": True,
"device": "/dev/ttyTEST123", "device": "/dev/ttyTEST123",
"baudrate": "115200", "baudrate": "115200",
"flow_control": True, "flow_control": True,
} }
}, ),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -336,16 +335,15 @@ async def test_option_flow_install_multi_pan_addon_zha(
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"
set_addon_options.assert_called_once_with( set_addon_options.assert_called_once_with(
hass,
"core_silabs_multiprotocol", "core_silabs_multiprotocol",
{ AddonsOptions(
"options": { config={
"autoflash_firmware": True, "autoflash_firmware": True,
"device": "/dev/ttyTEST123", "device": "/dev/ttyTEST123",
"baudrate": "115200", "baudrate": "115200",
"flow_control": True, "flow_control": True,
} }
}, ),
) )
# Check the channel is initialized from ZHA # Check the channel is initialized from ZHA
assert multipan_manager._channel == 11 assert multipan_manager._channel == 11
@ -424,16 +422,15 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio(
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"
set_addon_options.assert_called_once_with( set_addon_options.assert_called_once_with(
hass,
"core_silabs_multiprotocol", "core_silabs_multiprotocol",
{ AddonsOptions(
"options": { config={
"autoflash_firmware": True, "autoflash_firmware": True,
"device": "/dev/ttyTEST123", "device": "/dev/ttyTEST123",
"baudrate": "115200", "baudrate": "115200",
"flow_control": True, "flow_control": True,
} }
}, ),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1204,7 +1201,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails(
) -> None: ) -> None:
"""Test installing the multi pan addon.""" """Test installing the multi pan addon."""
install_addon.side_effect = HassioAPIError("Boom") install_addon.side_effect = SupervisorError("Boom")
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
@ -1283,16 +1280,15 @@ async def test_option_flow_install_multi_pan_addon_start_fails(
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"
set_addon_options.assert_called_once_with( set_addon_options.assert_called_once_with(
hass,
"core_silabs_multiprotocol", "core_silabs_multiprotocol",
{ AddonsOptions(
"options": { config={
"autoflash_firmware": True, "autoflash_firmware": True,
"device": "/dev/ttyTEST123", "device": "/dev/ttyTEST123",
"baudrate": "115200", "baudrate": "115200",
"flow_control": True, "flow_control": True,
} }
}, ),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1317,7 +1313,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails(
) -> None: ) -> None:
"""Test installing the multi pan addon.""" """Test installing the multi pan addon."""
set_addon_options.side_effect = HassioAPIError("Boom") set_addon_options.side_effect = SupervisorError("Boom")
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
@ -1361,7 +1357,7 @@ async def test_option_flow_addon_info_fails(
) -> None: ) -> None:
"""Test installing the multi pan addon.""" """Test installing the multi pan addon."""
addon_store_info.side_effect = HassioAPIError("Boom") addon_store_info.side_effect = SupervisorError("Boom")
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
@ -1494,16 +1490,15 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2(
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"
set_addon_options.assert_called_once_with( set_addon_options.assert_called_once_with(
hass,
"core_silabs_multiprotocol", "core_silabs_multiprotocol",
{ AddonsOptions(
"options": { config={
"autoflash_firmware": True, "autoflash_firmware": True,
"device": "/dev/ttyTEST123", "device": "/dev/ttyTEST123",
"baudrate": "115200", "baudrate": "115200",
"flow_control": True, "flow_control": True,
} }
}, ),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1668,7 +1663,7 @@ async def test_check_multi_pan_addon_info_error(
) -> None: ) -> None:
"""Test `check_multi_pan_addon` where the addon info cannot be read.""" """Test `check_multi_pan_addon` where the addon info cannot be read."""
addon_store_info.side_effect = HassioAPIError("Boom") addon_store_info.side_effect = SupervisorError("Boom")
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await silabs_multiprotocol_addon.check_multi_pan_addon(hass) await silabs_multiprotocol_addon.check_multi_pan_addon(hass)

View File

@ -1318,7 +1318,7 @@ async def test_addon_not_installed_failures(
install_addon: AsyncMock, install_addon: AsyncMock,
) -> None: ) -> None:
"""Test add-on install failure.""" """Test add-on install failure."""
install_addon.side_effect = HassioAPIError() install_addon.side_effect = SupervisorError()
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -1355,7 +1355,7 @@ async def test_addon_not_installed_failures_zeroconf(
zeroconf_info: ZeroconfServiceInfo, zeroconf_info: ZeroconfServiceInfo,
) -> None: ) -> None:
"""Test add-on install failure.""" """Test add-on install failure."""
install_addon.side_effect = HassioAPIError() install_addon.side_effect = SupervisorError()
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info

View File

@ -389,7 +389,7 @@ async def test_addon_info_failure(
True, True,
1, 1,
1, 1,
HassioAPIError("Boom"), SupervisorError("Boom"),
None, None,
ServerVersionTooOld("Invalid version"), ServerVersionTooOld("Invalid version"),
), ),

View File

@ -14,11 +14,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.hassio import ( from homeassistant.components.hassio import AddonError, HassioServiceInfo
AddonError,
HassioAPIError,
HassioServiceInfo,
)
from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED
from homeassistant.const import ( from homeassistant.const import (
CONF_CLIENT_ID, CONF_CLIENT_ID,
@ -253,7 +249,7 @@ async def test_user_connection_works(
assert len(mock_finish_setup.mock_calls) == 1 assert len(mock_finish_setup.mock_calls) == 1
@pytest.mark.usefixtures("mqtt_client_mock", "supervisor") @pytest.mark.usefixtures("mqtt_client_mock", "supervisor", "supervisor_client")
async def test_user_connection_works_with_supervisor( async def test_user_connection_works_with_supervisor(
hass: HomeAssistant, hass: HomeAssistant,
mock_try_connection: MagicMock, mock_try_connection: MagicMock,
@ -856,7 +852,7 @@ async def test_addon_not_installed_failures(
Case: The Mosquitto add-on install fails. Case: The Mosquitto add-on install fails.
""" """
install_addon.side_effect = HassioAPIError() install_addon.side_effect = SupervisorError()
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"mqtt", context={"source": config_entries.SOURCE_USER} "mqtt", context={"source": config_entries.SOURCE_USER}

View File

@ -47,6 +47,7 @@ def enable_mocks_fixture(
"""Enable API mocks.""" """Enable API mocks."""
@pytest.mark.usefixtures("supervisor_client")
async def test_import_dataset( async def test_import_dataset(
hass: HomeAssistant, hass: HomeAssistant,
mock_async_zeroconf: MagicMock, mock_async_zeroconf: MagicMock,
@ -201,6 +202,7 @@ async def test_import_share_radio_no_channel_collision(
) )
@pytest.mark.usefixtures("supervisor_client")
@pytest.mark.parametrize("enable_compute_pskc", [True]) @pytest.mark.parametrize("enable_compute_pskc", [True])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"dataset", [DATASET_INSECURE_NW_KEY, DATASET_INSECURE_PASSPHRASE] "dataset", [DATASET_INSECURE_NW_KEY, DATASET_INSECURE_PASSPHRASE]
@ -310,6 +312,7 @@ async def test_config_entry_update(hass: HomeAssistant) -> None:
mock_otrb_api.assert_called_once_with(new_config_entry_data["url"], ANY, ANY) mock_otrb_api.assert_called_once_with(new_config_entry_data["url"], ANY, ANY)
@pytest.mark.usefixtures("supervisor_client")
async def test_remove_entry( async def test_remove_entry(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry_multipan
) -> None: ) -> None:

View File

@ -1,6 +1,6 @@
"""Test OTBR Silicon Labs Multiprotocol support.""" """Test OTBR Silicon Labs Multiprotocol support."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pytest import pytest
from python_otbr_api import ActiveDataSet, tlv_parser from python_otbr_api import ActiveDataSet, tlv_parser
@ -31,6 +31,11 @@ DATASET_CH16_PENDING = (
) )
@pytest.fixture(autouse=True)
def mock_supervisor_client(supervisor_client: AsyncMock) -> None:
"""Mock supervisor client."""
async def test_async_change_channel( async def test_async_change_channel(
hass: HomeAssistant, otbr_config_entry_multipan hass: HomeAssistant, otbr_config_entry_multipan
) -> None: ) -> None:

View File

@ -13,6 +13,11 @@ OTBR_MULTIPAN_URL = "http://core-silabs-multiprotocol:8081"
OTBR_NON_MULTIPAN_URL = "/dev/ttyAMA1" OTBR_NON_MULTIPAN_URL = "/dev/ttyAMA1"
@pytest.fixture(autouse=True)
def mock_supervisor_client(supervisor_client: AsyncMock) -> None:
"""Mock supervisor client."""
async def test_get_allowed_channel( async def test_get_allowed_channel(
hass: HomeAssistant, multiprotocol_addon_manager_mock hass: HomeAssistant, multiprotocol_addon_manager_mock
) -> None: ) -> None:

View File

@ -1,6 +1,6 @@
"""Test OTBR Websocket API.""" """Test OTBR Websocket API."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pytest import pytest
import python_otbr_api import python_otbr_api
@ -29,6 +29,11 @@ async def websocket_client(
return await hass_ws_client(hass) return await hass_ws_client(hass)
@pytest.fixture(autouse=True)
def mock_supervisor_client(supervisor_client: AsyncMock) -> None:
"""Mock supervisor client."""
async def test_get_info( async def test_get_info(
hass: HomeAssistant, hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,

View File

@ -121,6 +121,11 @@ def backup(make_backup):
return make_backup() return make_backup()
@pytest.fixture(autouse=True)
def mock_supervisor_client(supervisor_client: AsyncMock) -> None:
"""Mock supervisor client."""
def mock_detect_radio_type( def mock_detect_radio_type(
radio_type: RadioType = RadioType.ezsp, radio_type: RadioType = RadioType.ezsp,
ret: ProbeResult = ProbeResult.RADIO_TYPE_DETECTED, ret: ProbeResult = ProbeResult.RADIO_TYPE_DETECTED,
@ -772,6 +777,7 @@ async def test_user_flow_show_form(hass: HomeAssistant) -> None:
assert result["step_id"] == "choose_serial_port" assert result["step_id"] == "choose_serial_port"
@pytest.mark.usefixtures("addon_not_installed")
@patch("serial.tools.list_ports.comports", MagicMock(return_value=[])) @patch("serial.tools.list_ports.comports", MagicMock(return_value=[]))
async def test_user_flow_show_manual(hass: HomeAssistant) -> None: async def test_user_flow_show_manual(hass: HomeAssistant) -> None:
"""Test user flow manual entry when no comport detected.""" """Test user flow manual entry when no comport detected."""

View File

@ -7,6 +7,8 @@ from ipaddress import ip_address
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock, call, patch from unittest.mock import AsyncMock, MagicMock, call, patch
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import AddonsOptions
import aiohttp import aiohttp
import pytest import pytest
from serial.tools.list_ports_common import ListPortInfo from serial.tools.list_ports_common import ListPortInfo
@ -601,10 +603,9 @@ async def test_usb_discovery(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": USB_DISCOVERY_INFO.device, "device": USB_DISCOVERY_INFO.device,
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -613,7 +614,7 @@ async def test_usb_discovery(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -702,10 +703,9 @@ async def test_usb_discovery_addon_not_running(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": USB_DISCOVERY_INFO.device, "device": USB_DISCOVERY_INFO.device,
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -714,7 +714,7 @@ async def test_usb_discovery_addon_not_running(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -797,10 +797,9 @@ async def test_discovery_addon_not_running(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/test", "device": "/test",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -809,7 +808,7 @@ async def test_discovery_addon_not_running(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -900,10 +899,9 @@ async def test_discovery_addon_not_installed(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/test", "device": "/test",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -912,7 +910,7 @@ async def test_discovery_addon_not_installed(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -1182,7 +1180,7 @@ async def test_addon_running(
{"config": ADDON_DISCOVERY_INFO}, {"config": ADDON_DISCOVERY_INFO},
None, None,
None, None,
HassioAPIError(), SupervisorError(),
"addon_info_failed", "addon_info_failed",
), ),
], ],
@ -1313,10 +1311,9 @@ async def test_addon_installed(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/test", "device": "/test",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -1325,7 +1322,7 @@ async def test_addon_installed(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -1366,7 +1363,7 @@ async def test_addon_installed(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("discovery_info", "start_addon_side_effect"), ("discovery_info", "start_addon_side_effect"),
[({"config": ADDON_DISCOVERY_INFO}, HassioAPIError())], [({"config": ADDON_DISCOVERY_INFO}, SupervisorError())],
) )
async def test_addon_installed_start_failure( async def test_addon_installed_start_failure(
hass: HomeAssistant, hass: HomeAssistant,
@ -1407,10 +1404,9 @@ async def test_addon_installed_start_failure(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/test", "device": "/test",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -1419,7 +1415,7 @@ async def test_addon_installed_start_failure(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -1486,10 +1482,9 @@ async def test_addon_installed_failures(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/test", "device": "/test",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -1498,7 +1493,7 @@ async def test_addon_installed_failures(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -1515,7 +1510,7 @@ async def test_addon_installed_failures(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("set_addon_options_side_effect", "discovery_info"), ("set_addon_options_side_effect", "discovery_info"),
[(HassioAPIError(), {"config": ADDON_DISCOVERY_INFO})], [(SupervisorError(), {"config": ADDON_DISCOVERY_INFO})],
) )
async def test_addon_installed_set_options_failure( async def test_addon_installed_set_options_failure(
hass: HomeAssistant, hass: HomeAssistant,
@ -1556,10 +1551,9 @@ async def test_addon_installed_set_options_failure(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/test", "device": "/test",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -1568,7 +1562,7 @@ async def test_addon_installed_set_options_failure(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
@ -1634,10 +1628,9 @@ async def test_addon_installed_already_configured(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/new", "device": "/new",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -1646,7 +1639,7 @@ async def test_addon_installed_already_configured(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -1719,10 +1712,9 @@ async def test_addon_not_installed(
) )
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{ AddonsOptions(
"options": { config={
"device": "/test", "device": "/test",
"s0_legacy_key": "new123", "s0_legacy_key": "new123",
"s2_access_control_key": "new456", "s2_access_control_key": "new456",
@ -1731,7 +1723,7 @@ async def test_addon_not_installed(
"lr_s2_access_control_key": "new654", "lr_s2_access_control_key": "new654",
"lr_s2_authenticated_key": "new321", "lr_s2_authenticated_key": "new321",
} }
}, ),
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -1774,7 +1766,7 @@ async def test_install_addon_failure(
hass: HomeAssistant, supervisor, addon_not_installed, install_addon hass: HomeAssistant, supervisor, addon_not_installed, install_addon
) -> None: ) -> None:
"""Test add-on install failure.""" """Test add-on install failure."""
install_addon.side_effect = HassioAPIError() install_addon.side_effect = SupervisorError()
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -1994,9 +1986,8 @@ async def test_options_addon_running(
new_addon_options["device"] = new_addon_options.pop("usb_path") new_addon_options["device"] = new_addon_options.pop("usb_path")
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass,
"core_zwave_js", "core_zwave_js",
{"options": new_addon_options}, AddonsOptions(config=new_addon_options),
) )
assert client.disconnect.call_count == disconnect_calls assert client.disconnect.call_count == disconnect_calls
@ -2275,9 +2266,7 @@ async def test_options_different_device(
assert set_addon_options.call_count == 1 assert set_addon_options.call_count == 1
new_addon_options["device"] = new_addon_options.pop("usb_path") new_addon_options["device"] = new_addon_options.pop("usb_path")
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "core_zwave_js", AddonsOptions(config=new_addon_options)
"core_zwave_js",
{"options": new_addon_options},
) )
assert client.disconnect.call_count == disconnect_calls assert client.disconnect.call_count == disconnect_calls
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -2298,9 +2287,7 @@ async def test_options_different_device(
assert set_addon_options.call_count == 2 assert set_addon_options.call_count == 2
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "core_zwave_js", AddonsOptions(config=addon_options)
"core_zwave_js",
{"options": addon_options},
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"
@ -2357,7 +2344,7 @@ async def test_options_different_device(
"emulate_hardware": False, "emulate_hardware": False,
}, },
0, 0,
[HassioAPIError(), None], [SupervisorError(), None],
), ),
( (
{"config": ADDON_DISCOVERY_INFO}, {"config": ADDON_DISCOVERY_INFO},
@ -2387,8 +2374,8 @@ async def test_options_different_device(
}, },
0, 0,
[ [
HassioAPIError(), SupervisorError(),
HassioAPIError(), SupervisorError(),
], ],
), ),
], ],
@ -2441,9 +2428,7 @@ async def test_options_addon_restart_failed(
assert set_addon_options.call_count == 1 assert set_addon_options.call_count == 1
new_addon_options["device"] = new_addon_options.pop("usb_path") new_addon_options["device"] = new_addon_options.pop("usb_path")
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "core_zwave_js", AddonsOptions(config=new_addon_options)
"core_zwave_js",
{"options": new_addon_options},
) )
assert client.disconnect.call_count == disconnect_calls assert client.disconnect.call_count == disconnect_calls
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
@ -2461,9 +2446,7 @@ async def test_options_addon_restart_failed(
old_addon_options.pop("network_key") old_addon_options.pop("network_key")
assert set_addon_options.call_count == 2 assert set_addon_options.call_count == 2
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "core_zwave_js", AddonsOptions(config=old_addon_options)
"core_zwave_js",
{"options": old_addon_options},
) )
assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon" assert result["step_id"] == "start_addon"
@ -2697,9 +2680,7 @@ async def test_options_addon_not_installed(
new_addon_options["device"] = new_addon_options.pop("usb_path") new_addon_options["device"] = new_addon_options.pop("usb_path")
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "core_zwave_js", AddonsOptions(config=new_addon_options)
"core_zwave_js",
{"options": new_addon_options},
) )
assert client.disconnect.call_count == disconnect_calls assert client.disconnect.call_count == disconnect_calls

View File

@ -6,6 +6,7 @@ import logging
from unittest.mock import AsyncMock, call, patch from unittest.mock import AsyncMock, call, patch
from aiohasupervisor import SupervisorError from aiohasupervisor import SupervisorError
from aiohasupervisor.models import AddonsOptions
import pytest import pytest
from zwave_js_server.client import Client from zwave_js_server.client import Client
from zwave_js_server.event import Event from zwave_js_server.event import Event
@ -554,7 +555,7 @@ async def test_start_addon(
assert install_addon.call_count == 0 assert install_addon.call_count == 0
assert set_addon_options.call_count == 1 assert set_addon_options.call_count == 1
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "core_zwave_js", {"options": addon_options} "core_zwave_js", AddonsOptions(config=addon_options)
) )
assert start_addon.call_count == 1 assert start_addon.call_count == 1
assert start_addon.call_args == call("core_zwave_js") assert start_addon.call_args == call("core_zwave_js")
@ -603,13 +604,13 @@ async def test_install_addon(
assert install_addon.call_args == call("core_zwave_js") assert install_addon.call_args == call("core_zwave_js")
assert set_addon_options.call_count == 1 assert set_addon_options.call_count == 1
assert set_addon_options.call_args == call( assert set_addon_options.call_args == call(
hass, "core_zwave_js", {"options": addon_options} "core_zwave_js", AddonsOptions(config=addon_options)
) )
assert start_addon.call_count == 1 assert start_addon.call_count == 1
assert start_addon.call_args == call("core_zwave_js") assert start_addon.call_args == call("core_zwave_js")
@pytest.mark.parametrize("addon_info_side_effect", [HassioAPIError("Boom")]) @pytest.mark.parametrize("addon_info_side_effect", [SupervisorError("Boom")])
async def test_addon_info_failure( async def test_addon_info_failure(
hass: HomeAssistant, hass: HomeAssistant,
addon_installed, addon_installed,
@ -747,7 +748,7 @@ async def test_addon_options_changed(
[ [
("1.0.0", True, 1, 1, None, None), ("1.0.0", True, 1, 1, None, None),
("1.0.0", False, 0, 0, None, None), ("1.0.0", False, 0, 0, None, None),
("1.0.0", True, 1, 1, HassioAPIError("Boom"), None), ("1.0.0", True, 1, 1, SupervisorError("Boom"), None),
("1.0.0", True, 0, 1, None, HassioAPIError("Boom")), ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")),
], ],
) )