mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-20 02:18:59 +00:00
Add availability API for addons (#6140)
* Add availability API for addons * Add cast back and test for latest version of installed addon * Make error responses more translation/client library friendly * Add test cases for install/update APIs
This commit is contained in:
@@ -67,9 +67,9 @@ from ..docker.monitor import DockerContainerStateEvent
|
|||||||
from ..docker.stats import DockerStats
|
from ..docker.stats import DockerStats
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
AddonConfigurationError,
|
AddonConfigurationError,
|
||||||
|
AddonNotSupportedError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
AddonsJobError,
|
AddonsJobError,
|
||||||
AddonsNotSupportedError,
|
|
||||||
ConfigurationFileError,
|
ConfigurationFileError,
|
||||||
DockerError,
|
DockerError,
|
||||||
HomeAssistantAPIError,
|
HomeAssistantAPIError,
|
||||||
@@ -1172,7 +1172,7 @@ class Addon(AddonModel):
|
|||||||
async def write_stdin(self, data) -> None:
|
async def write_stdin(self, data) -> None:
|
||||||
"""Write data to add-on stdin."""
|
"""Write data to add-on stdin."""
|
||||||
if not self.with_stdin:
|
if not self.with_stdin:
|
||||||
raise AddonsNotSupportedError(
|
raise AddonNotSupportedError(
|
||||||
f"Add-on {self.slug} does not support writing to stdin!", _LOGGER.error
|
f"Add-on {self.slug} does not support writing to stdin!", _LOGGER.error
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1419,7 +1419,7 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# If available
|
# If available
|
||||||
if not self._available(data[ATTR_SYSTEM]):
|
if not self._available(data[ATTR_SYSTEM]):
|
||||||
raise AddonsNotSupportedError(
|
raise AddonNotSupportedError(
|
||||||
f"Add-on {self.slug} is not available for this platform",
|
f"Add-on {self.slug} is not available for this platform",
|
||||||
_LOGGER.error,
|
_LOGGER.error,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ from supervisor.jobs.const import JobConcurrency
|
|||||||
from ..const import AddonBoot, AddonStartup, AddonState
|
from ..const import AddonBoot, AddonStartup, AddonState
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
|
AddonNotSupportedError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
AddonsJobError,
|
AddonsJobError,
|
||||||
AddonsNotSupportedError,
|
|
||||||
CoreDNSError,
|
CoreDNSError,
|
||||||
DockerError,
|
DockerError,
|
||||||
HassioError,
|
HassioError,
|
||||||
@@ -307,7 +307,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"Version changed, use Update instead Rebuild", _LOGGER.error
|
"Version changed, use Update instead Rebuild", _LOGGER.error
|
||||||
)
|
)
|
||||||
if not force and not addon.need_build:
|
if not force and not addon.need_build:
|
||||||
raise AddonsNotSupportedError(
|
raise AddonNotSupportedError(
|
||||||
"Can't rebuild a image based add-on", _LOGGER.error
|
"Can't rebuild a image based add-on", _LOGGER.error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,12 @@ from ..const import (
|
|||||||
)
|
)
|
||||||
from ..coresys import CoreSys
|
from ..coresys import CoreSys
|
||||||
from ..docker.const import Capabilities
|
from ..docker.const import Capabilities
|
||||||
from ..exceptions import AddonsNotSupportedError
|
from ..exceptions import (
|
||||||
|
AddonNotSupportedArchitectureError,
|
||||||
|
AddonNotSupportedError,
|
||||||
|
AddonNotSupportedHomeAssistantVersionError,
|
||||||
|
AddonNotSupportedMachineTypeError,
|
||||||
|
)
|
||||||
from ..jobs.const import JOB_GROUP_ADDON
|
from ..jobs.const import JOB_GROUP_ADDON
|
||||||
from ..jobs.job_group import JobGroup
|
from ..jobs.job_group import JobGroup
|
||||||
from ..utils import version_is_new_enough
|
from ..utils import version_is_new_enough
|
||||||
@@ -680,9 +685,8 @@ class AddonModel(JobGroup, ABC):
|
|||||||
"""Validate if addon is available for current system."""
|
"""Validate if addon is available for current system."""
|
||||||
# Architecture
|
# Architecture
|
||||||
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
|
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
|
||||||
raise AddonsNotSupportedError(
|
raise AddonNotSupportedArchitectureError(
|
||||||
f"Add-on {self.slug} not supported on this platform, supported architectures: {', '.join(config[ATTR_ARCH])}",
|
logger, slug=self.slug, architectures=config[ATTR_ARCH]
|
||||||
logger,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Machine / Hardware
|
# Machine / Hardware
|
||||||
@@ -690,9 +694,8 @@ class AddonModel(JobGroup, ABC):
|
|||||||
if machine and (
|
if machine and (
|
||||||
f"!{self.sys_machine}" in machine or self.sys_machine not in machine
|
f"!{self.sys_machine}" in machine or self.sys_machine not in machine
|
||||||
):
|
):
|
||||||
raise AddonsNotSupportedError(
|
raise AddonNotSupportedMachineTypeError(
|
||||||
f"Add-on {self.slug} not supported on this machine, supported machine types: {', '.join(machine)}",
|
logger, slug=self.slug, machine_types=machine
|
||||||
logger,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Home Assistant
|
# Home Assistant
|
||||||
@@ -701,16 +704,15 @@ class AddonModel(JobGroup, ABC):
|
|||||||
if version and not version_is_new_enough(
|
if version and not version_is_new_enough(
|
||||||
self.sys_homeassistant.version, version
|
self.sys_homeassistant.version, version
|
||||||
):
|
):
|
||||||
raise AddonsNotSupportedError(
|
raise AddonNotSupportedHomeAssistantVersionError(
|
||||||
f"Add-on {self.slug} not supported on this system, requires Home Assistant version {version} or greater",
|
logger, slug=self.slug, version=str(version)
|
||||||
logger,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _available(self, config) -> bool:
|
def _available(self, config) -> bool:
|
||||||
"""Return True if this add-on is available on this platform."""
|
"""Return True if this add-on is available on this platform."""
|
||||||
try:
|
try:
|
||||||
self._validate_availability(config)
|
self._validate_availability(config)
|
||||||
except AddonsNotSupportedError:
|
except AddonNotSupportedError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -735,6 +735,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"/store/addons/{addon}/documentation",
|
"/store/addons/{addon}/documentation",
|
||||||
api_store.addons_addon_documentation,
|
api_store.addons_addon_documentation,
|
||||||
),
|
),
|
||||||
|
web.get(
|
||||||
|
"/store/addons/{addon}/availability",
|
||||||
|
api_store.addons_addon_availability,
|
||||||
|
),
|
||||||
web.post(
|
web.post(
|
||||||
"/store/addons/{addon}/install", api_store.addons_addon_install
|
"/store/addons/{addon}/install", api_store.addons_addon_install
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -326,6 +326,12 @@ class APIStore(CoreSysAttributes):
|
|||||||
_read_static_text_file, addon.path_documentation
|
_read_static_text_file, addon.path_documentation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def addons_addon_availability(self, request: web.Request) -> None:
|
||||||
|
"""Check add-on availability for current system."""
|
||||||
|
addon = cast(AddonStore, self._extract_addon(request))
|
||||||
|
addon.validate_availability()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def repositories_list(self, request: web.Request) -> list[dict[str, Any]]:
|
async def repositories_list(self, request: web.Request) -> list[dict[str, Any]]:
|
||||||
"""Return all repositories."""
|
"""Return all repositories."""
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ from ..const import (
|
|||||||
HEADER_TOKEN,
|
HEADER_TOKEN,
|
||||||
HEADER_TOKEN_OLD,
|
HEADER_TOKEN_OLD,
|
||||||
JSON_DATA,
|
JSON_DATA,
|
||||||
|
JSON_ERROR_KEY,
|
||||||
|
JSON_EXTRA_FIELDS,
|
||||||
JSON_JOB_ID,
|
JSON_JOB_ID,
|
||||||
JSON_MESSAGE,
|
JSON_MESSAGE,
|
||||||
|
JSON_MESSAGE_TEMPLATE,
|
||||||
JSON_RESULT,
|
JSON_RESULT,
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
RESULT_ERROR,
|
RESULT_ERROR,
|
||||||
@@ -136,10 +139,11 @@ def api_process_raw(content, *, error_type=None):
|
|||||||
|
|
||||||
|
|
||||||
def api_return_error(
|
def api_return_error(
|
||||||
error: Exception | None = None,
|
error: HassioError | None = None,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
error_type: str | None = None,
|
error_type: str | None = None,
|
||||||
status: int = 400,
|
status: int = 400,
|
||||||
|
*,
|
||||||
job_id: str | None = None,
|
job_id: str | None = None,
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
"""Return an API error message."""
|
"""Return an API error message."""
|
||||||
@@ -158,12 +162,18 @@ def api_return_error(
|
|||||||
body=message.encode(), content_type=error_type, status=status
|
body=message.encode(), content_type=error_type, status=status
|
||||||
)
|
)
|
||||||
case _:
|
case _:
|
||||||
result = {
|
result: dict[str, Any] = {
|
||||||
JSON_RESULT: RESULT_ERROR,
|
JSON_RESULT: RESULT_ERROR,
|
||||||
JSON_MESSAGE: message,
|
JSON_MESSAGE: message,
|
||||||
}
|
}
|
||||||
if job_id:
|
if job_id:
|
||||||
result[JSON_JOB_ID] = job_id
|
result[JSON_JOB_ID] = job_id
|
||||||
|
if error and error.error_key:
|
||||||
|
result[JSON_ERROR_KEY] = error.error_key
|
||||||
|
if error and error.message_template:
|
||||||
|
result[JSON_MESSAGE_TEMPLATE] = error.message_template
|
||||||
|
if error and error.extra_fields:
|
||||||
|
result[JSON_EXTRA_FIELDS] = error.extra_fields
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
result,
|
result,
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ JSON_DATA = "data"
|
|||||||
JSON_MESSAGE = "message"
|
JSON_MESSAGE = "message"
|
||||||
JSON_RESULT = "result"
|
JSON_RESULT = "result"
|
||||||
JSON_JOB_ID = "job_id"
|
JSON_JOB_ID = "job_id"
|
||||||
|
JSON_ERROR_KEY = "error_key"
|
||||||
|
JSON_MESSAGE_TEMPLATE = "message_template"
|
||||||
|
JSON_EXTRA_FIELDS = "extra_fields"
|
||||||
|
|
||||||
RESULT_ERROR = "error"
|
RESULT_ERROR = "error"
|
||||||
RESULT_OK = "ok"
|
RESULT_OK = "ok"
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
"""Core Exceptions."""
|
"""Core Exceptions."""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class HassioError(Exception):
|
class HassioError(Exception):
|
||||||
"""Root exception."""
|
"""Root exception."""
|
||||||
|
|
||||||
|
error_key: str | None = None
|
||||||
|
message_template: str | None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
logger: Callable[..., None] | None = None,
|
logger: Callable[..., None] | None = None,
|
||||||
|
*,
|
||||||
|
extra_fields: dict[str, Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Raise & log."""
|
"""Raise & log."""
|
||||||
|
self.extra_fields = extra_fields or {}
|
||||||
|
|
||||||
|
if not message and self.message_template:
|
||||||
|
message = (
|
||||||
|
self.message_template.format(**self.extra_fields)
|
||||||
|
if self.extra_fields
|
||||||
|
else self.message_template
|
||||||
|
)
|
||||||
|
|
||||||
if logger is not None and message is not None:
|
if logger is not None and message is not None:
|
||||||
logger(message)
|
logger(message)
|
||||||
|
|
||||||
@@ -235,8 +250,71 @@ class AddonConfigurationError(AddonsError):
|
|||||||
"""Error with add-on configuration."""
|
"""Error with add-on configuration."""
|
||||||
|
|
||||||
|
|
||||||
class AddonsNotSupportedError(HassioNotSupportedError):
|
class AddonNotSupportedError(HassioNotSupportedError):
|
||||||
"""Addons don't support a function."""
|
"""Addon doesn't support a function."""
|
||||||
|
|
||||||
|
|
||||||
|
class AddonNotSupportedArchitectureError(AddonNotSupportedError):
|
||||||
|
"""Addon does not support system due to architecture."""
|
||||||
|
|
||||||
|
error_key = "addon_not_supported_architecture_error"
|
||||||
|
message_template = "Add-on {slug} not supported on this platform, supported architectures: {architectures}"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
logger: Callable[..., None] | None = None,
|
||||||
|
*,
|
||||||
|
slug: str,
|
||||||
|
architectures: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize exception."""
|
||||||
|
super().__init__(
|
||||||
|
None,
|
||||||
|
logger,
|
||||||
|
extra_fields={"slug": slug, "architectures": ", ".join(architectures)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddonNotSupportedMachineTypeError(AddonNotSupportedError):
|
||||||
|
"""Addon does not support system due to machine type."""
|
||||||
|
|
||||||
|
error_key = "addon_not_supported_machine_type_error"
|
||||||
|
message_template = "Add-on {slug} not supported on this machine, supported machine types: {machine_types}"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
logger: Callable[..., None] | None = None,
|
||||||
|
*,
|
||||||
|
slug: str,
|
||||||
|
machine_types: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize exception."""
|
||||||
|
super().__init__(
|
||||||
|
None,
|
||||||
|
logger,
|
||||||
|
extra_fields={"slug": slug, "machine_types": ", ".join(machine_types)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddonNotSupportedHomeAssistantVersionError(AddonNotSupportedError):
|
||||||
|
"""Addon does not support system due to Home Assistant version."""
|
||||||
|
|
||||||
|
error_key = "addon_not_supported_home_assistant_version_error"
|
||||||
|
message_template = "Add-on {slug} not supported on this system, requires Home Assistant version {version} or greater"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
logger: Callable[..., None] | None = None,
|
||||||
|
*,
|
||||||
|
slug: str,
|
||||||
|
version: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize exception."""
|
||||||
|
super().__init__(
|
||||||
|
None,
|
||||||
|
logger,
|
||||||
|
extra_fields={"slug": slug, "version": version},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AddonsJobError(AddonsError, JobException):
|
class AddonsJobError(AddonsError, JobException):
|
||||||
@@ -319,10 +397,17 @@ class APIError(HassioError, RuntimeError):
|
|||||||
self,
|
self,
|
||||||
message: str | None = None,
|
message: str | None = None,
|
||||||
logger: Callable[..., None] | None = None,
|
logger: Callable[..., None] | None = None,
|
||||||
|
*,
|
||||||
job_id: str | None = None,
|
job_id: str | None = None,
|
||||||
|
error: HassioError | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Raise & log, optionally with job."""
|
"""Raise & log, optionally with job."""
|
||||||
super().__init__(message, logger)
|
# Allow these to be set from another error here since APIErrors essentially wrap others to add a status
|
||||||
|
self.error_key = error.error_key if error else None
|
||||||
|
self.message_template = error.message_template if error else None
|
||||||
|
super().__init__(
|
||||||
|
message, logger, extra_fields=error.extra_fields if error else None
|
||||||
|
)
|
||||||
self.job_id = job_id
|
self.job_id = job_id
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
|||||||
|
|
||||||
from aiohttp import ClientResponse
|
from aiohttp import ClientResponse
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from supervisor.addons.addon import Addon
|
from supervisor.addons.addon import Addon
|
||||||
@@ -18,6 +19,7 @@ from supervisor.docker.addon import DockerAddon
|
|||||||
from supervisor.docker.const import ContainerState
|
from supervisor.docker.const import ContainerState
|
||||||
from supervisor.docker.interface import DockerInterface
|
from supervisor.docker.interface import DockerInterface
|
||||||
from supervisor.docker.monitor import DockerContainerStateEvent
|
from supervisor.docker.monitor import DockerContainerStateEvent
|
||||||
|
from supervisor.homeassistant.module import HomeAssistant
|
||||||
from supervisor.store.addon import AddonStore
|
from supervisor.store.addon import AddonStore
|
||||||
from supervisor.store.repository import Repository
|
from supervisor.store.repository import Repository
|
||||||
|
|
||||||
@@ -306,6 +308,7 @@ async def get_message(resp: ClientResponse, json_expected: bool) -> str:
|
|||||||
("post", "/store/addons/bad/install/1", True),
|
("post", "/store/addons/bad/install/1", True),
|
||||||
("post", "/store/addons/bad/update", True),
|
("post", "/store/addons/bad/update", True),
|
||||||
("post", "/store/addons/bad/update/1", True),
|
("post", "/store/addons/bad/update/1", True),
|
||||||
|
("get", "/store/addons/bad/availability", True),
|
||||||
# Legacy paths
|
# Legacy paths
|
||||||
("get", "/addons/bad/icon", False),
|
("get", "/addons/bad/icon", False),
|
||||||
("get", "/addons/bad/logo", False),
|
("get", "/addons/bad/logo", False),
|
||||||
@@ -492,3 +495,226 @@ async def test_background_addon_update_fails_fast(
|
|||||||
assert resp.status == 400
|
assert resp.status == 400
|
||||||
body = await resp.json()
|
body = await resp.json()
|
||||||
assert body["message"] == "No update available for add-on local_ssh"
|
assert body["message"] == "No update available for add-on local_ssh"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_store_addons_addon_availability_success(
|
||||||
|
api_client: TestClient, store_addon: AddonStore
|
||||||
|
):
|
||||||
|
"""Test /store/addons/{addon}/availability REST API - success case."""
|
||||||
|
resp = await api_client.get(f"/store/addons/{store_addon.slug}/availability")
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("supported_architectures", "api_action", "api_method", "installed"),
|
||||||
|
[
|
||||||
|
(["i386"], "availability", "get", False),
|
||||||
|
(["i386", "aarch64"], "availability", "get", False),
|
||||||
|
(["i386"], "install", "post", False),
|
||||||
|
(["i386", "aarch64"], "install", "post", False),
|
||||||
|
(["i386"], "update", "post", True),
|
||||||
|
(["i386", "aarch64"], "update", "post", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_api_store_addons_addon_availability_arch_not_supported(
|
||||||
|
api_client: TestClient,
|
||||||
|
coresys: CoreSys,
|
||||||
|
supported_architectures: list[str],
|
||||||
|
api_action: str,
|
||||||
|
api_method: str,
|
||||||
|
installed: bool,
|
||||||
|
):
|
||||||
|
"""Test availability errors for /store/addons/{addon}/* REST APIs - architecture not supported."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
# Create an addon with unsupported architecture
|
||||||
|
addon_obj = AddonStore(coresys, "test_arch_addon")
|
||||||
|
coresys.addons.store[addon_obj.slug] = addon_obj
|
||||||
|
|
||||||
|
# Set addon config with unsupported architecture
|
||||||
|
addon_config = {
|
||||||
|
"advanced": False,
|
||||||
|
"arch": supported_architectures,
|
||||||
|
"slug": "test_arch_addon",
|
||||||
|
"description": "Test arch add-on",
|
||||||
|
"name": "Test Arch Add-on",
|
||||||
|
"repository": "test",
|
||||||
|
"stage": "stable",
|
||||||
|
"version": "1.0.0",
|
||||||
|
}
|
||||||
|
coresys.store.data.addons[addon_obj.slug] = addon_config
|
||||||
|
if installed:
|
||||||
|
coresys.addons.local[addon_obj.slug] = Addon(coresys, addon_obj.slug)
|
||||||
|
coresys.addons.data.user[addon_obj.slug] = {"version": AwesomeVersion("0.0.1")}
|
||||||
|
|
||||||
|
# Mock the system architecture to be different
|
||||||
|
with patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])):
|
||||||
|
resp = await api_client.request(
|
||||||
|
api_method, f"/store/addons/{addon_obj.slug}/{api_action}"
|
||||||
|
)
|
||||||
|
assert resp.status == 400
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["error_key"] == "addon_not_supported_architecture_error"
|
||||||
|
assert (
|
||||||
|
result["message_template"]
|
||||||
|
== "Add-on {slug} not supported on this platform, supported architectures: {architectures}"
|
||||||
|
)
|
||||||
|
assert result["extra_fields"] == {
|
||||||
|
"slug": "test_arch_addon",
|
||||||
|
"architectures": ", ".join(supported_architectures),
|
||||||
|
}
|
||||||
|
assert result["message"] == result["message_template"].format(
|
||||||
|
**result["extra_fields"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("supported_machines", "api_action", "api_method", "installed"),
|
||||||
|
[
|
||||||
|
(["odroid-n2"], "availability", "get", False),
|
||||||
|
(["!qemux86-64"], "availability", "get", False),
|
||||||
|
(["a", "b"], "availability", "get", False),
|
||||||
|
(["odroid-n2"], "install", "post", False),
|
||||||
|
(["!qemux86-64"], "install", "post", False),
|
||||||
|
(["a", "b"], "install", "post", False),
|
||||||
|
(["odroid-n2"], "update", "post", True),
|
||||||
|
(["!qemux86-64"], "update", "post", True),
|
||||||
|
(["a", "b"], "update", "post", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_api_store_addons_addon_availability_machine_not_supported(
|
||||||
|
api_client: TestClient,
|
||||||
|
coresys: CoreSys,
|
||||||
|
supported_machines: list[str],
|
||||||
|
api_action: str,
|
||||||
|
api_method: str,
|
||||||
|
installed: bool,
|
||||||
|
):
|
||||||
|
"""Test availability errors for /store/addons/{addon}/* REST APIs - machine not supported."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
# Create an addon with unsupported machine type
|
||||||
|
addon_obj = AddonStore(coresys, "test_machine_addon")
|
||||||
|
coresys.addons.store[addon_obj.slug] = addon_obj
|
||||||
|
|
||||||
|
# Set addon config with unsupported machine
|
||||||
|
addon_config = {
|
||||||
|
"advanced": False,
|
||||||
|
"arch": ["amd64"],
|
||||||
|
"machine": supported_machines,
|
||||||
|
"slug": "test_machine_addon",
|
||||||
|
"description": "Test machine add-on",
|
||||||
|
"name": "Test Machine Add-on",
|
||||||
|
"repository": "test",
|
||||||
|
"stage": "stable",
|
||||||
|
"version": "1.0.0",
|
||||||
|
}
|
||||||
|
coresys.store.data.addons[addon_obj.slug] = addon_config
|
||||||
|
if installed:
|
||||||
|
coresys.addons.local[addon_obj.slug] = Addon(coresys, addon_obj.slug)
|
||||||
|
coresys.addons.data.user[addon_obj.slug] = {"version": AwesomeVersion("0.0.1")}
|
||||||
|
|
||||||
|
# Mock the system machine to be different
|
||||||
|
with patch.object(CoreSys, "machine", new=PropertyMock(return_value="qemux86-64")):
|
||||||
|
resp = await api_client.request(
|
||||||
|
api_method, f"/store/addons/{addon_obj.slug}/{api_action}"
|
||||||
|
)
|
||||||
|
assert resp.status == 400
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["error_key"] == "addon_not_supported_machine_type_error"
|
||||||
|
assert (
|
||||||
|
result["message_template"]
|
||||||
|
== "Add-on {slug} not supported on this machine, supported machine types: {machine_types}"
|
||||||
|
)
|
||||||
|
assert result["extra_fields"] == {
|
||||||
|
"slug": "test_machine_addon",
|
||||||
|
"machine_types": ", ".join(supported_machines),
|
||||||
|
}
|
||||||
|
assert result["message"] == result["message_template"].format(
|
||||||
|
**result["extra_fields"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("api_action", "api_method", "installed"),
|
||||||
|
[
|
||||||
|
("availability", "get", False),
|
||||||
|
("install", "post", False),
|
||||||
|
("update", "post", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_api_store_addons_addon_availability_homeassistant_version_too_old(
|
||||||
|
api_client: TestClient,
|
||||||
|
coresys: CoreSys,
|
||||||
|
api_action: str,
|
||||||
|
api_method: str,
|
||||||
|
installed: bool,
|
||||||
|
):
|
||||||
|
"""Test availability errors for /store/addons/{addon}/* REST APIs - Home Assistant version too old."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
# Create an addon that requires newer Home Assistant version
|
||||||
|
addon_obj = AddonStore(coresys, "test_version_addon")
|
||||||
|
coresys.addons.store[addon_obj.slug] = addon_obj
|
||||||
|
|
||||||
|
# Set addon config with minimum Home Assistant version requirement
|
||||||
|
addon_config = {
|
||||||
|
"advanced": False,
|
||||||
|
"arch": ["amd64"],
|
||||||
|
"homeassistant": "2023.1.1", # Requires newer version than current
|
||||||
|
"slug": "test_version_addon",
|
||||||
|
"description": "Test version add-on",
|
||||||
|
"name": "Test Version Add-on",
|
||||||
|
"repository": "test",
|
||||||
|
"stage": "stable",
|
||||||
|
"version": "1.0.0",
|
||||||
|
}
|
||||||
|
coresys.store.data.addons[addon_obj.slug] = addon_config
|
||||||
|
if installed:
|
||||||
|
coresys.addons.local[addon_obj.slug] = Addon(coresys, addon_obj.slug)
|
||||||
|
coresys.addons.data.user[addon_obj.slug] = {"version": AwesomeVersion("0.0.1")}
|
||||||
|
|
||||||
|
# Mock the Home Assistant version to be older
|
||||||
|
with patch.object(
|
||||||
|
HomeAssistant,
|
||||||
|
"version",
|
||||||
|
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
|
||||||
|
):
|
||||||
|
resp = await api_client.request(
|
||||||
|
api_method, f"/store/addons/{addon_obj.slug}/{api_action}"
|
||||||
|
)
|
||||||
|
assert resp.status == 400
|
||||||
|
result = await resp.json()
|
||||||
|
assert result["error_key"] == "addon_not_supported_home_assistant_version_error"
|
||||||
|
assert (
|
||||||
|
result["message_template"]
|
||||||
|
== "Add-on {slug} not supported on this system, requires Home Assistant version {version} or greater"
|
||||||
|
)
|
||||||
|
assert result["extra_fields"] == {
|
||||||
|
"slug": "test_version_addon",
|
||||||
|
"version": "2023.1.1",
|
||||||
|
}
|
||||||
|
assert result["message"] == result["message_template"].format(
|
||||||
|
**result["extra_fields"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_store_addons_addon_availability_installed_addon(
|
||||||
|
api_client: TestClient, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test /store/addons/{addon}/availability REST API - installed addon checks against latest version."""
|
||||||
|
resp = await api_client.get("/store/addons/local_ssh/availability")
|
||||||
|
assert resp.status == 200
|
||||||
|
|
||||||
|
install_addon_ssh.data_store["version"] = AwesomeVersion("10.0.0")
|
||||||
|
install_addon_ssh.data_store["homeassistant"] = AwesomeVersion("2023.1.1")
|
||||||
|
|
||||||
|
# Mock the Home Assistant version to be older
|
||||||
|
with patch.object(
|
||||||
|
HomeAssistant,
|
||||||
|
"version",
|
||||||
|
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
|
||||||
|
):
|
||||||
|
resp = await api_client.get("/store/addons/local_ssh/availability")
|
||||||
|
assert resp.status == 400
|
||||||
|
result = await resp.json()
|
||||||
|
assert (
|
||||||
|
"requires Home Assistant version 2023.1.1 or greater" in result["message"]
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from supervisor.addons.addon import Addon
|
|||||||
from supervisor.arch import CpuArch
|
from supervisor.arch import CpuArch
|
||||||
from supervisor.backups.manager import BackupManager
|
from supervisor.backups.manager import BackupManager
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.exceptions import AddonsNotSupportedError, StoreJobError
|
from supervisor.exceptions import AddonNotSupportedError, StoreJobError
|
||||||
from supervisor.homeassistant.module import HomeAssistant
|
from supervisor.homeassistant.module import HomeAssistant
|
||||||
from supervisor.store import StoreManager
|
from supervisor.store import StoreManager
|
||||||
from supervisor.store.addon import AddonStore
|
from supervisor.store.addon import AddonStore
|
||||||
@@ -172,7 +172,7 @@ async def test_update_unavailable_addon(
|
|||||||
),
|
),
|
||||||
patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))),
|
patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))),
|
||||||
):
|
):
|
||||||
with pytest.raises(AddonsNotSupportedError):
|
with pytest.raises(AddonNotSupportedError):
|
||||||
await coresys.addons.update("local_ssh", backup=True)
|
await coresys.addons.update("local_ssh", backup=True)
|
||||||
|
|
||||||
backup.assert_not_called()
|
backup.assert_not_called()
|
||||||
@@ -227,7 +227,7 @@ async def test_install_unavailable_addon(
|
|||||||
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
|
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
|
||||||
),
|
),
|
||||||
patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))),
|
patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))),
|
||||||
pytest.raises(AddonsNotSupportedError),
|
pytest.raises(AddonNotSupportedError),
|
||||||
):
|
):
|
||||||
await coresys.addons.install("local_ssh")
|
await coresys.addons.install("local_ssh")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user