1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-24 12:29:08 +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:
Mike Degatano
2025-09-04 05:14:42 -04:00
committed by GitHub
parent a3a5f6ba98
commit 7ed83a15fe
10 changed files with 360 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from aiohttp import ClientResponse
from aiohttp.test_utils import TestClient
from awesomeversion import AwesomeVersion
import pytest
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.interface import DockerInterface
from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.homeassistant.module import HomeAssistant
from supervisor.store.addon import AddonStore
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/update", True),
("post", "/store/addons/bad/update/1", True),
("get", "/store/addons/bad/availability", True),
# Legacy paths
("get", "/addons/bad/icon", False),
("get", "/addons/bad/logo", False),
@@ -492,3 +495,226 @@ async def test_background_addon_update_fails_fast(
assert resp.status == 400
body = await resp.json()
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"]
)