1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-17 11:33:49 +01:00
Files
core/tests/components/hassio/test_issues.py
T

1481 lines
44 KiB
Python

"""Test issues from supervisor issues."""
from collections.abc import Generator
import os
from typing import Any
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,
)
import pytest
from homeassistant.components.hassio.const import DOMAIN, HASSIO_ISSUES_UPDATE_INTERVAL
from homeassistant.components.hassio.coordinator import (
IssueSubscription,
IssueSubscriptionEvent,
get_issues_info,
)
from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from .test_init import MOCK_ENVIRON
from tests.common import async_fire_time_changed
from tests.typing import WebSocketGenerator
@pytest.fixture(autouse=True)
async def setup_repairs(hass: HomeAssistant) -> None:
"""Set up the repairs integration."""
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
@pytest.fixture(autouse=True)
def fixture_supervisor_environ() -> Generator[None]:
"""Mock os environ for supervisor."""
with patch.dict(os.environ, MOCK_ENVIRON):
yield
def mock_resolution_info(
supervisor_client: AsyncMock,
unsupported: list[UnsupportedReason | str] | None = None,
unhealthy: list[UnhealthyReason | str] | 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."""
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.DOCKER_CONFIG),
Check(enabled=True, slug=CheckType.FREE_SPACE),
],
)
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(
issues: list[dict[str, Any]], unhealthy: bool, reason: str
) -> None:
"""Assert repair for unhealthy/unsupported in list."""
repair_type = "unhealthy" if unhealthy else "unsupported"
assert {
"breaks_in_ha_version": None,
"created": ANY,
"dismissed_version": None,
"domain": "hassio",
"ignored": False,
"is_fixable": False,
"issue_id": f"{repair_type}_system_{reason}",
"issue_domain": None,
"learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}",
"severity": "critical" if unhealthy else "warning",
"translation_key": f"{repair_type}_{reason}",
"translation_placeholders": None,
} in issues
def assert_issue_repair_in_list(
issues: list[dict[str, Any]],
uuid: str,
context: str,
type_: str,
fixable: bool,
*,
reference: str | None = None,
placeholders: dict[str, str] | None = None,
) -> None:
"""Assert repair for unhealthy/unsupported in list."""
if reference:
placeholders = (placeholders or {}) | {"reference": reference}
assert {
"breaks_in_ha_version": None,
"created": ANY,
"dismissed_version": None,
"domain": "hassio",
"ignored": False,
"is_fixable": fixable,
"issue_id": uuid,
"issue_domain": None,
"learn_more_url": None,
"severity": "warning",
"translation_key": f"issue_{context}_{type_}",
"translation_placeholders": placeholders,
} in issues
@pytest.mark.usefixtures("all_setup_requests")
async def test_unhealthy_issues(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test issues added for unhealthy systems."""
mock_resolution_info(
supervisor_client, unhealthy=[UnhealthyReason.DOCKER, UnhealthyReason.SETUP]
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 2
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup")
@pytest.mark.usefixtures("all_setup_requests")
@pytest.mark.parametrize("unhealthy_reason", list(UnhealthyReason))
async def test_unhealthy_reasons(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
unhealthy_reason: UnhealthyReason,
) -> None:
"""Test all unhealthy reasons in client library are made into repairs."""
mock_resolution_info(supervisor_client, unhealthy=[unhealthy_reason])
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_repair_in_list(
msg["result"]["issues"], unhealthy=True, reason=unhealthy_reason.value
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_unsupported_issues(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test issues added for unsupported systems."""
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.CONNECTIVITY_CHECK, UnsupportedReason.OS],
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 2
assert_repair_in_list(
msg["result"]["issues"], unhealthy=False, reason="connectivity_check"
)
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
@pytest.mark.usefixtures("all_setup_requests")
@pytest.mark.parametrize(
"unsupported_reason",
[r for r in UnsupportedReason if r != UnsupportedReason.PRIVILEGED],
)
async def test_unsupported_reasons(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
unsupported_reason: UnsupportedReason,
) -> None:
"""Test all unsupported reasons in client library are made into repairs."""
mock_resolution_info(supervisor_client, unsupported=[unsupported_reason])
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_repair_in_list(
msg["result"]["issues"], unhealthy=False, reason=unsupported_reason.value
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_unhealthy_issues_add_remove(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test unhealthy issues added and removed from dispatches."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "health_changed",
"data": {
"healthy": False,
"unhealthy_reasons": ["docker"],
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
await client.send_json(
{
"id": 3,
"type": "supervisor/event",
"data": {
"event": "health_changed",
"data": {"healthy": True},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 4, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"issues": []}
@pytest.mark.usefixtures("all_setup_requests")
async def test_unsupported_issues_add_remove(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test unsupported issues added and removed from dispatches."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "supported_changed",
"data": {
"supported": False,
"unsupported_reasons": ["os"],
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
await client.send_json(
{
"id": 3,
"type": "supervisor/event",
"data": {
"event": "supported_changed",
"data": {"supported": True},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 4, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"issues": []}
@pytest.mark.usefixtures("all_setup_requests")
async def test_reset_issues_supervisor_restart(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""All issues reset on supervisor restart."""
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.OS],
unhealthy=[UnhealthyReason.DOCKER],
issues=[
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=(uuid := uuid4()),
)
],
suggestions_by_issue={
uuid: [
Suggestion(
SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
auto=False,
)
]
},
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 3
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=uuid.hex,
context="system",
type_="reboot_required",
fixable=True,
reference=None,
)
mock_resolution_info(supervisor_client)
await client.send_json(
{
"id": 2,
"type": "supervisor/event",
"data": {
"event": "supervisor_update",
"update_key": "supervisor",
"data": {"startup": "complete"},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 3, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"issues": []}
@pytest.mark.usefixtures("all_setup_requests")
async def test_no_reset_issues_supervisor_update_found(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Issues do not reset because a supervisor update was found."""
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.OS],
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
mock_resolution_info(supervisor_client)
await client.send_json(
{
"id": 2,
"type": "supervisor/event",
"data": {
"event": "supervisor_update",
"update_key": "supervisor",
"data": {},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 3, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
@pytest.mark.usefixtures("all_setup_requests")
async def test_reasons_added_and_removed(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test an unsupported/unhealthy reasons being added and removed at same time."""
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.OS],
unhealthy=[UnhealthyReason.DOCKER],
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 2
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.CONNECTIVITY_CHECK],
unhealthy=[UnhealthyReason.SETUP],
)
await client.send_json(
{
"id": 2,
"type": "supervisor/event",
"data": {
"event": "supervisor_update",
"update_key": "supervisor",
"data": {"startup": "complete"},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 3, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 2
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup")
assert_repair_in_list(
msg["result"]["issues"], unhealthy=False, reason="connectivity_check"
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_ignored_unsupported_skipped(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Unsupported reasons which have an identical unhealthy reason are ignored."""
mock_resolution_info(
supervisor_client,
unsupported=[UnsupportedReason.PRIVILEGED],
unhealthy=[UnhealthyReason.PRIVILEGED],
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="privileged")
@pytest.mark.usefixtures("all_setup_requests")
async def test_new_unsupported_unhealthy_reason(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""New unsupported/unhealthy reasons result in a generic repair."""
mock_resolution_info(
supervisor_client,
unsupported=["fake_unsupported"],
unhealthy=["fake_unhealthy"],
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 2
assert {
"breaks_in_ha_version": None,
"created": ANY,
"dismissed_version": None,
"domain": "hassio",
"ignored": False,
"is_fixable": False,
"issue_id": "unhealthy_system_fake_unhealthy",
"issue_domain": None,
"learn_more_url": "https://www.home-assistant.io/more-info/unhealthy/fake_unhealthy",
"severity": "critical",
"translation_key": "unhealthy",
"translation_placeholders": {"reason": "fake_unhealthy"},
} in msg["result"]["issues"]
assert {
"breaks_in_ha_version": None,
"created": ANY,
"dismissed_version": None,
"domain": "hassio",
"ignored": False,
"is_fixable": False,
"issue_id": "unsupported_system_fake_unsupported",
"issue_domain": None,
"learn_more_url": "https://www.home-assistant.io/more-info/unsupported/fake_unsupported",
"severity": "warning",
"translation_key": "unsupported",
"translation_placeholders": {"reason": "fake_unsupported"},
} in msg["result"]["issues"]
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test repairs added for supervisor issue."""
mock_resolution_info(
supervisor_client,
issues=[
Issue(
type=IssueType.DETACHED_ADDON_MISSING,
context=ContextType.ADDON,
reference="test",
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, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 2
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=uuid_issue1.hex,
context="addon",
type_="detached_addon_missing",
fixable=False,
reference="test",
placeholders={"addon_url": "/hassio/addon/test", "addon": "test"},
)
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=uuid_issue2.hex,
context="system",
type_="multiple_data_disks",
fixable=True,
reference="/dev/sda1",
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_initial_failure(
hass: HomeAssistant,
supervisor_client: AsyncMock,
resolution_info: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test initial issues refresh failure does not block hassio setup."""
mock_resolution_info(
supervisor_client,
unsupported=[],
unhealthy=[],
issues=[
Issue(
type=IssueType.REBOOT_REQUIRED,
context=ContextType.SYSTEM,
reference=None,
uuid=(uuid := uuid4()),
)
],
suggestions_by_issue={
uuid: [
Suggestion(
SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
auto=False,
)
]
},
)
resolution_info.side_effect = [
SupervisorBadRequestError("System is not ready with state: setup"),
resolution_info.return_value,
]
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 0
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_add_remove(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issues added and removed from dispatches."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "reboot_required",
"context": "system",
"reference": None,
"suggestions": [
{
"uuid": uuid4().hex,
"type": "execute_reboot",
"context": "system",
"reference": None,
}
],
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=issue_uuid,
context="system",
type_="reboot_required",
fixable=True,
reference=None,
)
await client.send_json(
{
"id": 3,
"type": "supervisor/event",
"data": {
"event": "issue_removed",
"data": {
"uuid": issue_uuid,
"type": "reboot_required",
"context": "system",
"reference": None,
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 4, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"issues": []}
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_suggestions_fail(
hass: HomeAssistant,
supervisor_client: AsyncMock,
resolution_suggestions_for_issue: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test failing to get suggestions for issue skips it."""
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, DOMAIN, {})
assert result
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 0
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_remove_missing_issue_without_error(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test HA skips message to remove issue that it didn't know about (sync issue)."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 5,
"type": "supervisor/event",
"data": {
"event": "issue_removed",
"data": {
"uuid": "1234",
"type": "reboot_required",
"context": "system",
"reference": None,
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
@pytest.mark.usefixtures("all_setup_requests")
async def test_system_is_not_ready(
hass: HomeAssistant,
resolution_info: AsyncMock,
) -> None:
"""Ensure hassio starts despite issues refresh errors."""
resolution_info.side_effect = SupervisorBadRequestError(
"System is not ready with state: setup"
)
assert await async_setup_component(hass, DOMAIN, {})
issues_coordinator = get_issues_info(hass)
assert issues_coordinator is not None
assert not issues_coordinator.issues
@pytest.mark.parametrize(
"all_setup_requests", [{"include_addons": True}], indirect=True
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_detached_addon_missing(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issue for detached addon due to missing repository."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "detached_addon_missing",
"context": "addon",
"reference": "test",
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=issue_uuid,
context="addon",
type_="detached_addon_missing",
fixable=False,
placeholders={
"reference": "test",
"addon": "test",
"addon_url": "/hassio/addon/test",
},
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_ntp_sync_failed(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issue for NTP sync failed."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "ntp_sync_failed",
"context": "system",
"reference": None,
"suggestions": [
{
"uuid": uuid4().hex,
"type": "enable_ntp",
"context": "system",
"reference": None,
}
],
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=issue_uuid,
context="system",
type_="ntp_sync_failed",
fixable=True,
placeholders=None,
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_disk_lifetime(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issue for disk lifetime nearly exceeded."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "disk_lifetime",
"context": "system",
"reference": None,
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=issue_uuid,
context="system",
type_="disk_lifetime",
fixable=False,
placeholders=None,
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_free_space(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issue for too little free space remaining."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "free_space",
"context": "system",
"reference": None,
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=issue_uuid,
context="system",
type_="free_space",
fixable=False,
placeholders={
"more_info_free_space": "https://www.home-assistant.io/more-info/free-space",
"storage_url": "/config/storage",
"free_space": "1.6",
},
)
@pytest.mark.parametrize(
"all_setup_requests", [{"include_addons": True}], indirect=True
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_addon_pwned(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issue for pwned secret in an addon."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "pwned",
"context": "addon",
"reference": "test",
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=issue_uuid,
context="addon",
type_="pwned",
fixable=False,
placeholders={
"reference": "test",
"addon": "test",
"addon_url": "/hassio/addon/test",
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
},
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_subscription_events(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test subscription callbacks for issue add/update/remove by key."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
issues_coordinator = get_issues_info(hass)
assert issues_coordinator is not None
events: list[str] = []
@callback
def _handle_subscription_event(event: IssueSubscriptionEvent) -> None:
events.append(event.event)
unsubscribe = issues_coordinator.subscribe(
IssueSubscription(
event_callback=_handle_subscription_event,
key="issue_system_should_not_be_repair",
)
)
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "should_not_be_repair",
"context": "system",
"reference": None,
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert events == ["changed"]
await client.send_json(
{
"id": 2,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": issue_uuid,
"type": "should_not_be_repair",
"context": "system",
"reference": "updated",
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert events == ["changed", "changed"]
await client.send_json(
{
"id": 3,
"type": "supervisor/event",
"data": {
"event": "issue_removed",
"data": {
"uuid": issue_uuid,
"type": "should_not_be_repair",
"context": "system",
"reference": "updated",
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert events == ["changed", "changed", "removed"]
unsubscribe()
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_periodic_refresh_backstop(
hass: HomeAssistant,
supervisor_client: AsyncMock,
) -> None:
"""Test issues coordinator polls periodically without requiring subscribers."""
mock_resolution_info(
supervisor_client,
issues=[
Issue(
type="should_not_be_repair",
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
)
],
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
issues_coordinator = get_issues_info(hass)
assert issues_coordinator is not None
supervisor_client.resolution.info.reset_mock()
async_fire_time_changed(hass, dt_util.utcnow() + HASSIO_ISSUES_UPDATE_INTERVAL)
await hass.async_block_till_done()
supervisor_client.resolution.info.assert_called_once()
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_suggestions_change_updates_fixable_state(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test suggestion-only issue changes are not treated as unchanged."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
supervisor_client.resolution.info.reset_mock()
issue_uuid = uuid4()
supervisor_client.resolution.info.return_value = ResolutionInfo(
unsupported=[],
unhealthy=[],
issues=[
Issue(
type="should_not_be_repair",
context=ContextType.SYSTEM,
reference=None,
uuid=issue_uuid,
)
],
suggestions=[
Suggestion(
type=SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
auto=False,
)
],
checks=[
Check(enabled=True, slug=CheckType.DOCKER_CONFIG),
Check(enabled=True, slug=CheckType.FREE_SPACE),
],
)
supervisor_client.resolution.suggestions_for_issue.return_value = [
Suggestion(
type=SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
auto=False,
)
]
issues_coordinator = get_issues_info(hass)
assert issues_coordinator is not None
events: list[str] = []
@callback
def _subscription_event(event: IssueSubscriptionEvent) -> None:
events.append(event.event)
unsubscribe = issues_coordinator.subscribe(
IssueSubscription(
event_callback=_subscription_event,
key="issue_system_should_not_be_repair",
)
)
supervisor_client_ws = await hass_supervisor_ws_client()
await supervisor_client_ws.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": issue_uuid.hex,
"type": "should_not_be_repair",
"context": "system",
"reference": None,
},
},
}
)
msg = await supervisor_client_ws.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert events == ["changed"]
await issues_coordinator.async_refresh()
await hass.async_block_till_done()
assert events == ["changed", "changed"]
unsubscribe()
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_periodic_refresh_recovers_after_initial_failure(
hass: HomeAssistant,
supervisor_client: AsyncMock,
resolution_info: AsyncMock,
) -> None:
"""Test a later refresh recovers issue state after initial refresh failure."""
issue_uuid = uuid4()
mock_resolution_info(
supervisor_client,
issues=[
Issue(
type="should_not_be_repair",
context=ContextType.SYSTEM,
reference=None,
uuid=issue_uuid,
)
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
SuggestionType.EXECUTE_REBOOT,
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
auto=False,
)
]
},
)
resolution_info.side_effect = [
SupervisorBadRequestError("System is not ready with state: setup"),
resolution_info.return_value,
]
result = await async_setup_component(hass, DOMAIN, {})
assert result
issues_coordinator = get_issues_info(hass)
assert issues_coordinator is not None
assert len(issues_coordinator.issues) == 0
await issues_coordinator.async_refresh()
await hass.async_block_till_done()
assert len(issues_coordinator.issues) == 1
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_unload_disconnects_listener(
hass: HomeAssistant,
supervisor_client: AsyncMock,
resolution_info: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test SupervisorIssues.unload() disconnects the EVENT_SUPERVISOR_EVENT listener.
After calling unload(), dispatching supervisor events must not trigger
the listener — preventing listener accumulation on config-entry reload.
"""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, DOMAIN, {})
assert result
# Get config entry
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
entry = entries[0]
# Get issues instance
issues = get_issues_info(hass)
assert issues is not None
assert issues.unhealthy_reasons == set()
# While connected, a health_changed event updates unhealthy_reasons.
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "health_changed",
"data": {"healthy": False, "unhealthy_reasons": ["docker"]},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert "docker" in issues.unhealthy_reasons
# After config entry unload, the same event is silently ignored.
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
await client.send_json(
{
"id": 2,
"type": "supervisor/event",
"data": {
"event": "health_changed",
"data": {"healthy": True},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
# unhealthy_reasons unchanged — listener did not fire.
assert "docker" in issues.unhealthy_reasons