Replace Supervisor resolution API calls with aiohasupervisor (#129599)

* Replace Supervisor resolution API calls with aiohasupervisor

* Use consistent types to avoid uuid issues

* Fix mocking in http test

* Changes from feedback

* Put hass first

* Fix typo

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Mike Degatano 2024-11-06 19:33:51 -05:00 committed by GitHub
parent 03d5b18974
commit ed4f55406c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 607 additions and 699 deletions

View File

@ -137,17 +137,3 @@ class SupervisorEntityModel(StrEnum):
CORE = "Home Assistant Core"
SUPERVIOSR = "Home Assistant Supervisor"
HOST = "Home Assistant Host"
class SupervisorIssueContext(StrEnum):
"""Context for supervisor issues."""
ADDON = "addon"
CORE = "core"
DNS_SERVER = "dns_server"
MOUNT = "mount"
OS = "os"
PLUGIN = "plugin"
SUPERVISOR = "supervisor"
STORE = "store"
SYSTEM = "system"

View File

@ -91,15 +91,6 @@ async def async_create_backup(
return await hassio.send_command(command, payload=payload, timeout=None)
@bind_hass
@_api_bool
async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> dict:
"""Apply a suggestion from supervisor's resolution center."""
hassio: HassIO = hass.data[DOMAIN]
command = f"/resolution/suggestion/{suggestion_uuid}"
return await hassio.send_command(command, timeout=None)
@api_data
async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]:
"""Return settings specific to Home Assistant Green."""
@ -245,26 +236,6 @@ class HassIO:
"""
return self.send_command("/ingress/panels", method="get")
@api_data
def get_resolution_info(self) -> Coroutine:
"""Return data for Supervisor resolution center.
This method returns a coroutine.
"""
return self.send_command("/resolution/info", method="get")
@api_data
def get_suggestions_for_issue(
self, issue_id: str
) -> Coroutine[Any, Any, dict[str, Any]]:
"""Return suggestions for issue from Supervisor resolution center.
This method returns a coroutine.
"""
return self.send_command(
f"/resolution/issue/{issue_id}/suggestions", method="get"
)
@_api_bool
async def update_hass_api(
self, http_config: dict[str, Any], refresh_token: RefreshToken
@ -304,14 +275,6 @@ class HassIO:
"/supervisor/options", payload={"diagnostics": diagnostics}
)
@_api_bool
def apply_suggestion(self, suggestion_uuid: str) -> Coroutine:
"""Apply a suggestion from supervisor's resolution center.
This method returns a coroutine.
"""
return self.send_command(f"/resolution/suggestion/{suggestion_uuid}")
async def send_command(
self,
command: str,

View File

@ -7,6 +7,10 @@ from dataclasses import dataclass, field
from datetime import datetime
import logging
from typing import Any, NotRequired, TypedDict
from uuid import UUID
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import ContextType, Issue as SupervisorIssue
from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -20,12 +24,8 @@ from homeassistant.helpers.issue_registry import (
from .const import (
ATTR_DATA,
ATTR_HEALTHY,
ATTR_ISSUES,
ATTR_SUGGESTIONS,
ATTR_SUPPORTED,
ATTR_UNHEALTHY,
ATTR_UNHEALTHY_REASONS,
ATTR_UNSUPPORTED,
ATTR_UNSUPPORTED_REASONS,
ATTR_UPDATE_KEY,
ATTR_WS_EVENT,
@ -45,10 +45,9 @@ from .const import (
PLACEHOLDER_KEY_REFERENCE,
REQUEST_REFRESH_DELAY,
UPDATE_KEY_SUPERVISOR,
SupervisorIssueContext,
)
from .coordinator import get_addons_info
from .handler import HassIO, HassioAPIError
from .handler import HassIO, get_supervisor_client
ISSUE_KEY_UNHEALTHY = "unhealthy"
ISSUE_KEY_UNSUPPORTED = "unsupported"
@ -120,9 +119,9 @@ class SuggestionDataType(TypedDict):
class Suggestion:
"""Suggestion from Supervisor which resolves an issue."""
uuid: str
uuid: UUID
type: str
context: SupervisorIssueContext
context: ContextType
reference: str | None = None
@property
@ -134,9 +133,9 @@ class Suggestion:
def from_dict(cls, data: SuggestionDataType) -> Suggestion:
"""Convert from dictionary representation."""
return cls(
uuid=data["uuid"],
uuid=UUID(data["uuid"]),
type=data["type"],
context=SupervisorIssueContext(data["context"]),
context=ContextType(data["context"]),
reference=data["reference"],
)
@ -155,9 +154,9 @@ class IssueDataType(TypedDict):
class Issue:
"""Issue from Supervisor."""
uuid: str
uuid: UUID
type: str
context: SupervisorIssueContext
context: ContextType
reference: str | None = None
suggestions: list[Suggestion] = field(default_factory=list, compare=False)
@ -171,9 +170,9 @@ class Issue:
"""Convert from dictionary representation."""
suggestions: list[SuggestionDataType] = data.get("suggestions", [])
return cls(
uuid=data["uuid"],
uuid=UUID(data["uuid"]),
type=data["type"],
context=SupervisorIssueContext(data["context"]),
context=ContextType(data["context"]),
reference=data["reference"],
suggestions=[
Suggestion.from_dict(suggestion) for suggestion in suggestions
@ -190,7 +189,8 @@ class SupervisorIssues:
self._client = client
self._unsupported_reasons: set[str] = set()
self._unhealthy_reasons: set[str] = set()
self._issues: dict[str, Issue] = {}
self._issues: dict[UUID, Issue] = {}
self._supervisor_client = get_supervisor_client(hass)
@property
def unhealthy_reasons(self) -> set[str]:
@ -283,7 +283,7 @@ class SupervisorIssues:
async_create_issue(
self._hass,
DOMAIN,
issue.uuid,
issue.uuid.hex,
is_fixable=bool(issue.suggestions),
severity=IssueSeverity.WARNING,
translation_key=issue.key,
@ -292,19 +292,37 @@ class SupervisorIssues:
self._issues[issue.uuid] = issue
async def add_issue_from_data(self, data: IssueDataType) -> None:
async def add_issue_from_data(self, data: SupervisorIssue) -> None:
"""Add issue from data to list after getting latest suggestions."""
try:
data["suggestions"] = (
await self._client.get_suggestions_for_issue(data["uuid"])
)[ATTR_SUGGESTIONS]
except HassioAPIError:
suggestions = (
await self._supervisor_client.resolution.suggestions_for_issue(
data.uuid
)
)
except SupervisorError:
_LOGGER.error(
"Could not get suggestions for supervisor issue %s, skipping it",
data["uuid"],
data.uuid.hex,
)
return
self.add_issue(Issue.from_dict(data))
self.add_issue(
Issue(
uuid=data.uuid,
type=str(data.type),
context=data.context,
reference=data.reference,
suggestions=[
Suggestion(
uuid=suggestion.uuid,
type=str(suggestion.type),
context=suggestion.context,
reference=suggestion.reference,
)
for suggestion in suggestions
],
)
)
def remove_issue(self, issue: Issue) -> None:
"""Remove an issue from the list. Delete a repair if necessary."""
@ -312,13 +330,13 @@ class SupervisorIssues:
return
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
async_delete_issue(self._hass, DOMAIN, issue.uuid)
async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
del self._issues[issue.uuid]
def get_issue(self, issue_id: str) -> Issue | None:
"""Get issue from key."""
return self._issues.get(issue_id)
return self._issues.get(UUID(issue_id))
async def setup(self) -> None:
"""Create supervisor events listener."""
@ -331,8 +349,8 @@ class SupervisorIssues:
async def _update(self, _: datetime | None = None) -> None:
"""Update issues from Supervisor resolution center."""
try:
data = await self._client.get_resolution_info()
except HassioAPIError as err:
data = await self._supervisor_client.resolution.info()
except SupervisorError as err:
_LOGGER.error("Failed to update supervisor issues: %r", err)
async_call_later(
self._hass,
@ -340,18 +358,16 @@ class SupervisorIssues:
HassJob(self._update, cancel_on_shutdown=True),
)
return
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
self.unhealthy_reasons = set(data.unhealthy)
self.unsupported_reasons = set(data.unsupported)
# Remove any cached issues that weren't returned
for issue_id in set(self._issues.keys()) - {
issue["uuid"] for issue in data[ATTR_ISSUES]
}:
for issue_id in set(self._issues) - {issue.uuid for issue in data.issues}:
self.remove_issue(self._issues[issue_id])
# Add/update any issues that came back
await asyncio.gather(
*[self.add_issue_from_data(issue) for issue in data[ATTR_ISSUES]]
*[self.add_issue_from_data(issue) for issue in data.issues]
)
@callback

View File

@ -6,6 +6,8 @@ from collections.abc import Callable, Coroutine
from types import MethodType
from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import ContextType
import voluptuous as vol
from homeassistant.components.repairs import RepairsFlow
@ -20,9 +22,8 @@ from .const import (
PLACEHOLDER_KEY_ADDON,
PLACEHOLDER_KEY_COMPONENTS,
PLACEHOLDER_KEY_REFERENCE,
SupervisorIssueContext,
)
from .handler import async_apply_suggestion
from .handler import get_supervisor_client
from .issues import Issue, Suggestion
HELP_URLS = {
@ -51,9 +52,10 @@ class SupervisorIssueRepairFlow(RepairsFlow):
_data: dict[str, Any] | None = None
_issue: Issue | None = None
def __init__(self, issue_id: str) -> None:
def __init__(self, hass: HomeAssistant, issue_id: str) -> None:
"""Initialize repair flow."""
self._issue_id = issue_id
self._supervisor_client = get_supervisor_client(hass)
super().__init__()
@property
@ -124,9 +126,12 @@ class SupervisorIssueRepairFlow(RepairsFlow):
if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED:
return self._async_form_for_suggestion(suggestion)
if await async_apply_suggestion(self.hass, suggestion.uuid):
return self.async_create_entry(data={})
return self.async_abort(reason="apply_suggestion_fail")
try:
await self._supervisor_client.resolution.apply_suggestion(suggestion.uuid)
except SupervisorError:
return self.async_abort(reason="apply_suggestion_fail")
return self.async_create_entry(data={})
@staticmethod
def _async_step(
@ -163,9 +168,9 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
if issue.key == self.issue.key or issue.type != self.issue.type:
continue
if issue.context == SupervisorIssueContext.CORE:
if issue.context == ContextType.CORE:
components.insert(0, "Home Assistant")
elif issue.context == SupervisorIssueContext.ADDON:
elif issue.context == ContextType.ADDON:
components.append(
next(
(
@ -210,11 +215,11 @@ async def async_create_fix_flow(
supervisor_issues = get_issues_info(hass)
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
return DockerConfigIssueRepairFlow(issue_id)
return DockerConfigIssueRepairFlow(hass, issue_id)
if issue and issue.key in {
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_BOOT_FAIL,
}:
return AddonIssueRepairFlow(issue_id)
return AddonIssueRepairFlow(hass, issue_id)
return SupervisorIssueRepairFlow(issue_id)
return SupervisorIssueRepairFlow(hass, issue_id)

View File

@ -8,7 +8,13 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock, patch
from aiohasupervisor.models import Discovery, Repository, StoreAddon, StoreInfo
from aiohasupervisor.models import (
Discovery,
Repository,
ResolutionInfo,
StoreAddon,
StoreInfo,
)
import pytest
from homeassistant.config_entries import (
@ -473,6 +479,26 @@ def supervisor_is_connected_fixture(supervisor_client: AsyncMock) -> AsyncMock:
return supervisor_client.supervisor.ping
@pytest.fixture(name="resolution_info")
def resolution_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock resolution info from supervisor."""
supervisor_client.resolution.info.return_value = ResolutionInfo(
suggestions=[],
unsupported=[],
unhealthy=[],
issues=[],
checks=[],
)
return supervisor_client.resolution.info
@pytest.fixture(name="resolution_suggestions_for_issue")
def resolution_suggestions_for_issue_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock suggestions by issue from supervisor resolution."""
supervisor_client.resolution.suggestions_for_issue.return_value = []
return supervisor_client.resolution.suggestions_for_issue
@pytest.fixture(name="supervisor_client")
def supervisor_client() -> Generator[AsyncMock]:
"""Mock the supervisor client."""
@ -481,6 +507,7 @@ def supervisor_client() -> Generator[AsyncMock]:
supervisor_client.discovery = AsyncMock()
supervisor_client.homeassistant = AsyncMock()
supervisor_client.os = AsyncMock()
supervisor_client.resolution = AsyncMock()
supervisor_client.supervisor = AsyncMock()
with (
patch(
@ -504,7 +531,11 @@ def supervisor_client() -> Generator[AsyncMock]:
return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.get_supervisor_client",
"homeassistant.components.hassio.issues.get_supervisor_client",
return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.repairs.get_supervisor_client",
return_value=supervisor_client,
),
):

View File

@ -25,6 +25,7 @@ def mock_all(
store_info: AsyncMock,
addon_changelog: AsyncMock,
addon_stats: AsyncMock,
resolution_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@ -140,19 +141,6 @@ def mock_all(
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/network/info",
json={

View File

@ -24,6 +24,7 @@ def mock_all(
store_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@ -143,19 +144,6 @@ def mock_all(
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/network/info",
json={

View File

@ -208,7 +208,7 @@ async def test_api_ingress_panels(
@pytest.mark.parametrize(
("api_call", "method", "payload"),
[
("get_resolution_info", "GET", None),
("get_network_info", "GET", None),
("update_diagnostics", "POST", True),
],
)

View File

@ -67,6 +67,7 @@ def mock_all(
addon_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@ -204,19 +205,6 @@ def mock_all(
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/network/info",
json={

View File

@ -4,11 +4,28 @@ from __future__ import annotations
from collections.abc import Generator
from datetime import timedelta
from http import HTTPStatus
import os
from typing import Any
from unittest.mock import ANY, patch
from unittest.mock import ANY, AsyncMock, patch
from uuid import UUID, uuid4
from aiohasupervisor import (
SupervisorBadRequestError,
SupervisorError,
SupervisorTimeoutError,
)
from aiohasupervisor.models import (
Check,
CheckType,
ContextType,
Issue,
IssueType,
ResolutionInfo,
Suggestion,
SuggestionType,
UnhealthyReason,
UnsupportedReason,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
@ -18,7 +35,6 @@ from homeassistant.setup import async_setup_component
from .test_init import MOCK_ENVIRON
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
from tests.typing import WebSocketGenerator
@ -36,49 +52,41 @@ def fixture_supervisor_environ() -> Generator[None]:
def mock_resolution_info(
aioclient_mock: AiohttpClientMocker,
unsupported: list[str] | None = None,
unhealthy: list[str] | None = None,
issues: list[dict[str, str]] | None = None,
suggestion_result: str = "ok",
supervisor_client: AsyncMock,
unsupported: list[UnsupportedReason] | None = None,
unhealthy: list[UnhealthyReason] | None = None,
issues: list[Issue] | None = None,
suggestions_by_issue: dict[UUID, list[Suggestion]] | None = None,
suggestion_result: SupervisorError | None = None,
) -> None:
"""Mock resolution/info endpoint with unsupported/unhealthy reasons and/or issues."""
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": unsupported or [],
"unhealthy": unhealthy or [],
"suggestions": [],
"issues": [
{k: v for k, v in issue.items() if k != "suggestions"}
for issue in issues
]
if issues
else [],
"checks": [
{"enabled": True, "slug": "supervisor_trust"},
{"enabled": True, "slug": "free_space"},
],
},
},
supervisor_client.resolution.info.return_value = ResolutionInfo(
unsupported=unsupported or [],
unhealthy=unhealthy or [],
issues=issues or [],
suggestions=[
suggestion
for issue_list in suggestions_by_issue.values()
for suggestion in issue_list
]
if suggestions_by_issue
else [],
checks=[
Check(enabled=True, slug=CheckType.SUPERVISOR_TRUST),
Check(enabled=True, slug=CheckType.FREE_SPACE),
],
)
if issues:
suggestions_by_issue = {
issue["uuid"]: issue.get("suggestions", []) for issue in issues
}
for issue_uuid, suggestions in suggestions_by_issue.items():
aioclient_mock.get(
f"http://127.0.0.1/resolution/issue/{issue_uuid}/suggestions",
json={"result": "ok", "data": {"suggestions": suggestions}},
)
for suggestion in suggestions:
aioclient_mock.post(
f"http://127.0.0.1/resolution/suggestion/{suggestion['uuid']}",
json={"result": suggestion_result},
)
if suggestions_by_issue:
async def mock_suggestions_for_issue(uuid: UUID) -> list[Suggestion]:
"""Mock of suggestions for issue api."""
return suggestions_by_issue.get(uuid, [])
supervisor_client.resolution.suggestions_for_issue.side_effect = (
mock_suggestions_for_issue
)
supervisor_client.resolution.apply_suggestion.side_effect = suggestion_result
def assert_repair_in_list(
@ -134,11 +142,13 @@ def assert_issue_repair_in_list(
@pytest.mark.usefixtures("all_setup_requests")
async def test_unhealthy_issues(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test issues added for unhealthy systems."""
mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"])
mock_resolution_info(
supervisor_client, unhealthy=[UnhealthyReason.DOCKER, UnhealthyReason.SETUP]
)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -156,11 +166,14 @@ async def test_unhealthy_issues(
@pytest.mark.usefixtures("all_setup_requests")
async def test_unsupported_issues(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test issues added for unsupported systems."""
mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"])
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.CONTENT_TRUST, UnsupportedReason.OS],
)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -180,11 +193,11 @@ async def test_unsupported_issues(
@pytest.mark.usefixtures("all_setup_requests")
async def test_unhealthy_issues_add_remove(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test unhealthy issues added and removed from dispatches."""
mock_resolution_info(aioclient_mock)
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -237,11 +250,11 @@ async def test_unhealthy_issues_add_remove(
@pytest.mark.usefixtures("all_setup_requests")
async def test_unsupported_issues_add_remove(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test unsupported issues added and removed from dispatches."""
mock_resolution_info(aioclient_mock)
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -294,21 +307,21 @@ async def test_unsupported_issues_add_remove(
@pytest.mark.usefixtures("all_setup_requests")
async def test_reset_issues_supervisor_restart(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""All issues reset on supervisor restart."""
mock_resolution_info(
aioclient_mock,
unsupported=["os"],
unhealthy=["docker"],
supervisor_client,
unsupported=[UnsupportedReason.OS],
unhealthy=[UnhealthyReason.DOCKER],
issues=[
{
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": None,
}
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=(uuid := uuid4()),
)
],
)
@ -325,15 +338,14 @@ async def test_reset_issues_supervisor_restart(
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid="1234",
uuid=uuid.hex,
context="system",
type_="reboot_required",
fixable=False,
reference=None,
)
aioclient_mock.clear_requests()
mock_resolution_info(aioclient_mock)
mock_resolution_info(supervisor_client)
await client.send_json(
{
"id": 2,
@ -358,11 +370,15 @@ async def test_reset_issues_supervisor_restart(
@pytest.mark.usefixtures("all_setup_requests")
async def test_reasons_added_and_removed(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test an unsupported/unhealthy reasons being added and removed at same time."""
mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"])
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.OS],
unhealthy=[UnhealthyReason.DOCKER],
)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -376,9 +392,10 @@ async def test_reasons_added_and_removed(
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
aioclient_mock.clear_requests()
mock_resolution_info(
aioclient_mock, unsupported=["content_trust"], unhealthy=["setup"]
supervisor_client,
unsupported=[UnsupportedReason.CONTENT_TRUST],
unhealthy=[UnhealthyReason.SETUP],
)
await client.send_json(
{
@ -408,12 +425,14 @@ async def test_reasons_added_and_removed(
@pytest.mark.usefixtures("all_setup_requests")
async def test_ignored_unsupported_skipped(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Unsupported reasons which have an identical unhealthy reason are ignored."""
mock_resolution_info(
aioclient_mock, unsupported=["privileged"], unhealthy=["privileged"]
supervisor_client,
unsupported=[UnsupportedReason.PRIVILEGED],
unhealthy=[UnhealthyReason.PRIVILEGED],
)
result = await async_setup_component(hass, "hassio", {})
@ -431,12 +450,14 @@ async def test_ignored_unsupported_skipped(
@pytest.mark.usefixtures("all_setup_requests")
async def test_new_unsupported_unhealthy_reason(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""New unsupported/unhealthy reasons result in a generic repair until next core update."""
mock_resolution_info(
aioclient_mock, unsupported=["fake_unsupported"], unhealthy=["fake_unhealthy"]
supervisor_client,
unsupported=["fake_unsupported"],
unhealthy=["fake_unhealthy"],
)
result = await async_setup_component(hass, "hassio", {})
@ -481,40 +502,43 @@ async def test_new_unsupported_unhealthy_reason(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test repairs added for supervisor issue."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": None,
},
{
"uuid": "1235",
"type": "multiple_data_disks",
"context": "system",
"reference": "/dev/sda1",
"suggestions": [
{
"uuid": "1236",
"type": "rename_data_disk",
"context": "system",
"reference": "/dev/sda1",
}
],
},
{
"uuid": "1237",
"type": "should_not_be_repair",
"context": "os",
"reference": None,
},
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=(uuid_issue1 := uuid4()),
),
Issue(
type=IssueType.MULTIPLE_DATA_DISKS,
context=ContextType.SYSTEM,
reference="/dev/sda1",
uuid=(uuid_issue2 := uuid4()),
),
Issue(
type="should_not_be_repair",
context=ContextType.OS,
reference=None,
uuid=uuid4(),
),
],
suggestions_by_issue={
uuid_issue2: [
Suggestion(
type=SuggestionType.RENAME_DATA_DISK,
context=ContextType.SYSTEM,
reference="/dev/sda1",
uuid=uuid4(),
auto=False,
)
]
},
)
result = await async_setup_component(hass, "hassio", {})
@ -528,7 +552,7 @@ async def test_supervisor_issues(
assert len(msg["result"]["issues"]) == 2
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid="1234",
uuid=uuid_issue1.hex,
context="system",
type_="reboot_required",
fixable=False,
@ -536,7 +560,7 @@ async def test_supervisor_issues(
)
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid="1235",
uuid=uuid_issue2.hex,
context="system",
type_="multiple_data_disks",
fixable=True,
@ -547,61 +571,33 @@ async def test_supervisor_issues(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_initial_failure(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
resolution_info: AsyncMock,
resolution_suggestions_for_issue: AsyncMock,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test issues manager retries after initial update failure."""
responses = [
AiohttpClientMockResponse(
method="get",
url="http://127.0.0.1/resolution/info",
status=HTTPStatus.BAD_REQUEST,
json={
"result": "error",
"message": "System is not ready with state: setup",
},
),
AiohttpClientMockResponse(
method="get",
url="http://127.0.0.1/resolution/info",
status=HTTPStatus.OK,
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [
{
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": None,
},
],
"checks": [
{"enabled": True, "slug": "supervisor_trust"},
{"enabled": True, "slug": "free_space"},
],
},
},
resolution_info.side_effect = [
SupervisorBadRequestError("System is not ready with state: setup"),
ResolutionInfo(
unsupported=[],
unhealthy=[],
suggestions=[],
issues=[
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
)
],
checks=[
Check(enabled=True, slug=CheckType.SUPERVISOR_TRUST),
Check(enabled=True, slug=CheckType.FREE_SPACE),
],
),
]
async def mock_responses(*args):
nonlocal responses
return responses.pop(0)
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
side_effect=mock_responses,
)
aioclient_mock.get(
"http://127.0.0.1/resolution/issue/1234/suggestions",
json={"result": "ok", "data": {"suggestions": []}},
)
with patch("homeassistant.components.hassio.issues.REQUEST_REFRESH_DELAY", new=0.1):
result = await async_setup_component(hass, "hassio", {})
await hass.async_block_till_done()
@ -625,11 +621,11 @@ async def test_supervisor_issues_initial_failure(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_add_remove(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issues added and removed from dispatches."""
mock_resolution_info(aioclient_mock)
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -643,7 +639,7 @@ async def test_supervisor_issues_add_remove(
"data": {
"event": "issue_changed",
"data": {
"uuid": "1234",
"uuid": (issue_uuid := uuid4().hex),
"type": "reboot_required",
"context": "system",
"reference": None,
@ -661,7 +657,7 @@ async def test_supervisor_issues_add_remove(
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid="1234",
uuid=issue_uuid,
context="system",
type_="reboot_required",
fixable=False,
@ -675,13 +671,13 @@ async def test_supervisor_issues_add_remove(
"data": {
"event": "issue_changed",
"data": {
"uuid": "1234",
"uuid": issue_uuid,
"type": "reboot_required",
"context": "system",
"reference": None,
"suggestions": [
{
"uuid": "1235",
"uuid": uuid4().hex,
"type": "execute_reboot",
"context": "system",
"reference": None,
@ -701,7 +697,7 @@ async def test_supervisor_issues_add_remove(
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid="1234",
uuid=issue_uuid,
context="system",
type_="reboot_required",
fixable=True,
@ -715,7 +711,7 @@ async def test_supervisor_issues_add_remove(
"data": {
"event": "issue_removed",
"data": {
"uuid": "1234",
"uuid": issue_uuid,
"type": "reboot_required",
"context": "system",
"reference": None,
@ -736,37 +732,23 @@ async def test_supervisor_issues_add_remove(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_suggestions_fail(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
resolution_suggestions_for_issue: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test failing to get suggestions for issue skips it."""
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [
{
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": None,
}
],
"checks": [
{"enabled": True, "slug": "supervisor_trust"},
{"enabled": True, "slug": "free_space"},
],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/resolution/issue/1234/suggestions",
exc=TimeoutError(),
mock_resolution_info(
supervisor_client,
issues=[
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
)
],
)
resolution_suggestions_for_issue.side_effect = SupervisorTimeoutError
result = await async_setup_component(hass, "hassio", {})
assert result
@ -782,11 +764,11 @@ async def test_supervisor_issues_suggestions_fail(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_remove_missing_issue_without_error(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test HA skips message to remove issue that it didn't know about (sync issue)."""
mock_resolution_info(aioclient_mock)
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -816,16 +798,12 @@ async def test_supervisor_remove_missing_issue_without_error(
@pytest.mark.usefixtures("all_setup_requests")
async def test_system_is_not_ready(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
resolution_info: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Ensure hassio starts despite error."""
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "",
"message": "System is not ready with state: setup",
},
resolution_info.side_effect = SupervisorBadRequestError(
"System is not ready with state: setup"
)
assert await async_setup_component(hass, "hassio", {})
@ -838,11 +816,11 @@ async def test_system_is_not_ready(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_detached_addon_missing(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issue for detached addon due to missing repository."""
mock_resolution_info(aioclient_mock)
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, "hassio", {})
assert result
@ -856,7 +834,7 @@ async def test_supervisor_issues_detached_addon_missing(
"data": {
"event": "issue_changed",
"data": {
"uuid": "1234",
"uuid": (issue_uuid := uuid4().hex),
"type": "detached_addon_missing",
"context": "addon",
"reference": "test",
@ -874,7 +852,7 @@ async def test_supervisor_issues_detached_addon_missing(
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid="1234",
uuid=issue_uuid,
context="addon",
type_="detached_addon_missing",
fixable=False,

View File

@ -3,8 +3,17 @@
from collections.abc import Generator
from http import HTTPStatus
import os
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from uuid import uuid4
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import (
ContextType,
Issue,
IssueType,
Suggestion,
SuggestionType,
)
import pytest
from homeassistant.core import HomeAssistant
@ -14,7 +23,6 @@ from homeassistant.setup import async_setup_component
from .test_init import MOCK_ENVIRON
from .test_issues import mock_resolution_info
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator
@ -28,34 +36,39 @@ def fixture_supervisor_environ() -> Generator[None]:
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_repair_flow(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for supervisor issue."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "multiple_data_disks",
"context": "system",
"reference": "/dev/sda1",
"suggestions": [
{
"uuid": "1235",
"type": "rename_data_disk",
"context": "system",
"reference": "/dev/sda1",
}
],
},
Issue(
type=IssueType.MULTIPLE_DATA_DISKS,
context=ContextType.SYSTEM,
reference="/dev/sda1",
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.RENAME_DATA_DISK,
context=ContextType.SYSTEM,
reference="/dev/sda1",
uuid=(sugg_uuid := uuid4()),
auto=False,
)
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -95,52 +108,53 @@ async def test_supervisor_issue_repair_flow(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1235"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_repair_flow_with_multiple_suggestions(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for supervisor issue with multiple suggestions."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": "test",
"suggestions": [
{
"uuid": "1235",
"type": "execute_reboot",
"context": "system",
"reference": "test",
},
{
"uuid": "1236",
"type": "test_type",
"context": "system",
"reference": "test",
},
],
},
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference="test",
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference="test",
uuid=uuid4(),
auto=False,
),
Suggestion(
type="test_type",
context=ContextType.SYSTEM,
reference="test",
uuid=(sugg_uuid := uuid4()),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -189,52 +203,53 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1236"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confirmation(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for supervisor issue with multiple suggestions and choice requires confirmation."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": None,
"suggestions": [
{
"uuid": "1235",
"type": "execute_reboot",
"context": "system",
"reference": None,
},
{
"uuid": "1236",
"type": "test_type",
"context": "system",
"reference": None,
},
],
},
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference=None,
uuid=(sugg_uuid := uuid4()),
auto=False,
),
Suggestion(
type="test_type",
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -302,46 +317,46 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1235"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_repair_flow_skip_confirmation(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test confirmation skipped for fix flow for supervisor issue with one suggestion."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": None,
"suggestions": [
{
"uuid": "1235",
"type": "execute_reboot",
"context": "system",
"reference": None,
}
],
},
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference=None,
uuid=(sugg_uuid := uuid4()),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -381,53 +396,54 @@ async def test_supervisor_issue_repair_flow_skip_confirmation(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1235"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.usefixtures("all_setup_requests")
async def test_mount_failed_repair_flow_error(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair flow fails when repair fails to apply."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "mount_failed",
"context": "mount",
"reference": "backup_share",
"suggestions": [
{
"uuid": "1235",
"type": "execute_reload",
"context": "mount",
"reference": "backup_share",
},
{
"uuid": "1236",
"type": "execute_remove",
"context": "mount",
"reference": "backup_share",
},
],
},
Issue(
type=IssueType.MOUNT_FAILED,
context=ContextType.MOUNT,
reference="backup_share",
uuid=(issue_uuid := uuid4()),
),
],
suggestion_result=False,
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_RELOAD,
context=ContextType.MOUNT,
reference="backup_share",
uuid=uuid4(),
auto=False,
),
Suggestion(
type=SuggestionType.EXECUTE_REMOVE,
context=ContextType.MOUNT,
reference="backup_share",
uuid=uuid4(),
auto=False,
),
]
},
suggestion_result=SupervisorError("boom"),
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -459,46 +475,52 @@ async def test_mount_failed_repair_flow_error(
"description_placeholders": None,
}
assert issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
@pytest.mark.usefixtures("all_setup_requests")
async def test_mount_failed_repair_flow(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair flow for mount_failed issue."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "mount_failed",
"context": "mount",
"reference": "backup_share",
"suggestions": [
{
"uuid": "1235",
"type": "execute_reload",
"context": "mount",
"reference": "backup_share",
},
{
"uuid": "1236",
"type": "execute_remove",
"context": "mount",
"reference": "backup_share",
},
],
},
Issue(
type=IssueType.MOUNT_FAILED,
context=ContextType.MOUNT,
reference="backup_share",
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_RELOAD,
context=ContextType.MOUNT,
reference="backup_share",
uuid=(sugg_uuid := uuid4()),
auto=False,
),
Suggestion(
type=SuggestionType.EXECUTE_REMOVE,
context=ContextType.MOUNT,
reference="backup_share",
uuid=uuid4(),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -551,13 +573,8 @@ async def test_mount_failed_repair_flow(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1235"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.parametrize(
@ -566,62 +583,69 @@ async def test_mount_failed_repair_flow(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_docker_config_repair_flow(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for supervisor issue."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "docker_config",
"context": "system",
"reference": None,
"suggestions": [
{
"uuid": "1235",
"type": "execute_rebuild",
"context": "system",
"reference": None,
}
],
},
{
"uuid": "1236",
"type": "docker_config",
"context": "core",
"reference": None,
"suggestions": [
{
"uuid": "1237",
"type": "execute_rebuild",
"context": "core",
"reference": None,
}
],
},
{
"uuid": "1238",
"type": "docker_config",
"context": "addon",
"reference": "test",
"suggestions": [
{
"uuid": "1239",
"type": "execute_rebuild",
"context": "addon",
"reference": "test",
}
],
},
Issue(
type=IssueType.DOCKER_CONFIG,
context=ContextType.SYSTEM,
reference=None,
uuid=(issue1_uuid := uuid4()),
),
Issue(
type=IssueType.DOCKER_CONFIG,
context=ContextType.CORE,
reference=None,
uuid=(issue2_uuid := uuid4()),
),
Issue(
type=IssueType.DOCKER_CONFIG,
context=ContextType.ADDON,
reference="test",
uuid=(issue3_uuid := uuid4()),
),
],
suggestions_by_issue={
issue1_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REBUILD,
context=ContextType.SYSTEM,
reference=None,
uuid=(sugg_uuid := uuid4()),
auto=False,
),
],
issue2_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REBUILD,
context=ContextType.CORE,
reference=None,
uuid=uuid4(),
auto=False,
),
],
issue3_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REBUILD,
context=ContextType.ADDON,
reference="test",
uuid=uuid4(),
auto=False,
),
],
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue1_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -661,52 +685,53 @@ async def test_supervisor_issue_docker_config_repair_flow(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1235"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue1_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_repair_flow_multiple_data_disks(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for multiple data disks supervisor issue."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "multiple_data_disks",
"context": "system",
"reference": "/dev/sda1",
"suggestions": [
{
"uuid": "1235",
"type": "rename_data_disk",
"context": "system",
"reference": "/dev/sda1",
},
{
"uuid": "1236",
"type": "adopt_data_disk",
"context": "system",
"reference": "/dev/sda1",
},
],
},
Issue(
type=IssueType.MULTIPLE_DATA_DISKS,
context=ContextType.SYSTEM,
reference="/dev/sda1",
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.RENAME_DATA_DISK,
context=ContextType.SYSTEM,
reference="/dev/sda1",
uuid=uuid4(),
auto=False,
),
Suggestion(
type=SuggestionType.ADOPT_DATA_DISK,
context=ContextType.SYSTEM,
reference="/dev/sda1",
uuid=(sugg_uuid := uuid4()),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -774,13 +799,8 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1236"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.parametrize(
@ -789,34 +809,39 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_detached_addon_removed(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for supervisor issue."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "detached_addon_removed",
"context": "addon",
"reference": "test",
"suggestions": [
{
"uuid": "1235",
"type": "execute_remove",
"context": "addon",
"reference": "test",
}
],
},
Issue(
type=IssueType.DETACHED_ADDON_REMOVED,
context=ContextType.ADDON,
reference="test",
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type=SuggestionType.EXECUTE_REMOVE,
context=ContextType.ADDON,
reference="test",
uuid=(sugg_uuid := uuid4()),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -861,13 +886,8 @@ async def test_supervisor_issue_detached_addon_removed(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1235"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.parametrize(
@ -876,40 +896,46 @@ async def test_supervisor_issue_detached_addon_removed(
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_addon_boot_fail(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for supervisor issue."""
mock_resolution_info(
aioclient_mock,
supervisor_client,
issues=[
{
"uuid": "1234",
"type": "boot_fail",
"context": "addon",
"reference": "test",
"suggestions": [
{
"uuid": "1235",
"type": "execute_start",
"context": "addon",
"reference": "test",
},
{
"uuid": "1236",
"type": "disable_boot",
"context": "addon",
"reference": "test",
},
],
},
Issue(
type="boot_fail",
context=ContextType.ADDON,
reference="test",
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
type="execute_start",
context=ContextType.ADDON,
reference="test",
uuid=(sugg_uuid := uuid4()),
auto=False,
),
Suggestion(
type="disable_boot",
context=ContextType.ADDON,
reference="test",
uuid=uuid4(),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
@ -962,10 +988,5 @@ async def test_supervisor_issue_addon_boot_fail(
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
assert aioclient_mock.mock_calls[-1][0] == "post"
assert (
str(aioclient_mock.mock_calls[-1][1])
== "http://127.0.0.1/resolution/suggestion/1235"
)
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)

View File

@ -33,6 +33,7 @@ def mock_all(
store_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
_install_default_mocks(aioclient_mock)
@ -146,19 +147,6 @@ def _install_default_mocks(aioclient_mock: AiohttpClientMocker):
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/network/info",
json={

View File

@ -29,6 +29,7 @@ def mock_all(
store_info: AsyncMock,
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@ -149,19 +150,6 @@ def mock_all(
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/network/info",
json={

View File

@ -26,7 +26,9 @@ from tests.typing import WebSocketGenerator
@pytest.fixture(autouse=True)
def mock_all(
aioclient_mock: AiohttpClientMocker, supervisor_is_connected: AsyncMock
aioclient_mock: AiohttpClientMocker,
supervisor_is_connected: AsyncMock,
resolution_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@ -67,19 +69,6 @@ def mock_all(
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
},
)
@pytest.mark.usefixtures("hassio_env")

View File

@ -197,6 +197,7 @@ async def test_access_from_supervisor_ip(
hass: HomeAssistant,
aiohttp_client: ClientSessionGenerator,
hassio_env,
resolution_info: AsyncMock,
) -> None:
"""Test accessing to server from supervisor IP."""
app = web.Application()
@ -218,17 +219,7 @@ async def test_access_from_supervisor_ip(
manager = app[KEY_BAN_MANAGER]
with patch(
"homeassistant.components.hassio.HassIO.get_resolution_info",
return_value={
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
):
assert await async_setup_component(hass, "hassio", {"hassio": {}})
assert await async_setup_component(hass, "hassio", {"hassio": {}})
m_open = mock_open()

View File

@ -72,23 +72,11 @@ async def mock_supervisor_fixture(
aioclient_mock: AiohttpClientMocker,
store_info: AsyncMock,
supervisor_is_connected: AsyncMock,
resolution_info: AsyncMock,
) -> AsyncGenerator[None]:
"""Mock supervisor."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
aioclient_mock.get(
"http://127.0.0.1/resolution/info",
json={
"result": "ok",
"data": {
"unsupported": [],
"unhealthy": [],
"suggestions": [],
"issues": [],
"checks": [],
},
},
)
aioclient_mock.get(
"http://127.0.0.1/network/info",
json={