Teach resolution center about fixing issues (#74694)

This commit is contained in:
Erik Montnemery 2022-07-12 19:26:06 +02:00 committed by GitHub
parent cf612c4bec
commit b0fde206b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 485 additions and 17 deletions

View File

@ -4,16 +4,19 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import websocket_api from . import issue_handler, websocket_api
from .const import DOMAIN from .const import DOMAIN
from .issue_handler import async_create_issue, async_delete_issue from .issue_handler import ResolutionCenterFlow, async_create_issue, async_delete_issue
from .issue_registry import async_load as async_load_issue_registry from .issue_registry import async_load as async_load_issue_registry
__all__ = ["DOMAIN", "async_create_issue", "async_delete_issue"] __all__ = ["DOMAIN", "ResolutionCenterFlow", "async_create_issue", "async_delete_issue"]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Resolution Center.""" """Set up Resolution Center."""
hass.data[DOMAIN] = {}
issue_handler.async_setup(hass)
websocket_api.async_setup(hass) websocket_api.async_setup(hass)
await async_load_issue_registry(hass) await async_load_issue_registry(hass)

View File

@ -1,12 +1,85 @@
"""The resolution center integration.""" """The resolution center integration."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant import data_entry_flow
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
from .const import DOMAIN
from .issue_registry import async_get as async_get_issue_registry from .issue_registry import async_get as async_get_issue_registry
from .models import IssueSeverity from .models import IssueSeverity, ResolutionCenterFlow, ResolutionCenterProtocol
class ResolutionCenterFlowManager(data_entry_flow.FlowManager):
"""Manage resolution center flows."""
async def async_create_flow(
self,
handler_key: Any,
*,
context: dict[str, Any] | None = None,
data: dict[str, Any] | None = None,
) -> ResolutionCenterFlow:
"""Create a flow. platform is a resolution center module."""
if "platforms" not in self.hass.data[DOMAIN]:
await async_process_resolution_center_platforms(self.hass)
platforms: dict[str, ResolutionCenterProtocol] = self.hass.data[DOMAIN][
"platforms"
]
if handler_key not in platforms:
raise data_entry_flow.UnknownHandler
platform = platforms[handler_key]
assert data and "issue_id" in data
issue_id = data["issue_id"]
issue_registry = async_get_issue_registry(self.hass)
issue = issue_registry.async_get_issue(handler_key, issue_id)
if issue is None or not issue.is_fixable:
raise data_entry_flow.UnknownStep
return await platform.async_create_fix_flow(self.hass, issue_id)
async def async_finish_flow(
self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult
) -> data_entry_flow.FlowResult:
"""Complete a fix flow."""
async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"])
if "result" not in result:
result["result"] = None
return result
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Initialize resolution center."""
hass.data[DOMAIN]["flow_manager"] = ResolutionCenterFlowManager(hass)
async def async_process_resolution_center_platforms(hass: HomeAssistant) -> None:
"""Start processing resolution center platforms."""
hass.data[DOMAIN]["platforms"] = {}
await async_process_integration_platforms(
hass, DOMAIN, _register_resolution_center_platform
)
async def _register_resolution_center_platform(
hass: HomeAssistant, integration_domain: str, platform: ResolutionCenterProtocol
) -> None:
"""Register a resolution center platform."""
if not hasattr(platform, "async_create_fix_flow"):
raise HomeAssistantError(f"Invalid resolution center platform {platform}")
hass.data[DOMAIN]["platforms"][integration_domain] = platform
@callback @callback
@ -16,6 +89,7 @@ def async_create_issue(
issue_id: str, issue_id: str,
*, *,
breaks_in_ha_version: str | None = None, breaks_in_ha_version: str | None = None,
is_fixable: bool,
learn_more_url: str | None = None, learn_more_url: str | None = None,
severity: IssueSeverity, severity: IssueSeverity,
translation_key: str, translation_key: str,
@ -35,6 +109,7 @@ def async_create_issue(
domain, domain,
issue_id, issue_id,
breaks_in_ha_version=breaks_in_ha_version, breaks_in_ha_version=breaks_in_ha_version,
is_fixable=is_fixable,
learn_more_url=learn_more_url, learn_more_url=learn_more_url,
severity=severity, severity=severity,
translation_key=translation_key, translation_key=translation_key,

View File

@ -25,6 +25,7 @@ class IssueEntry:
breaks_in_ha_version: str | None breaks_in_ha_version: str | None
dismissed_version: str | None dismissed_version: str | None
domain: str domain: str
is_fixable: bool | None
issue_id: str issue_id: str
learn_more_url: str | None learn_more_url: str | None
severity: IssueSeverity | None severity: IssueSeverity | None
@ -55,6 +56,7 @@ class IssueRegistry:
issue_id: str, issue_id: str,
*, *,
breaks_in_ha_version: str | None = None, breaks_in_ha_version: str | None = None,
is_fixable: bool,
learn_more_url: str | None = None, learn_more_url: str | None = None,
severity: IssueSeverity, severity: IssueSeverity,
translation_key: str, translation_key: str,
@ -68,6 +70,7 @@ class IssueRegistry:
breaks_in_ha_version=breaks_in_ha_version, breaks_in_ha_version=breaks_in_ha_version,
dismissed_version=None, dismissed_version=None,
domain=domain, domain=domain,
is_fixable=is_fixable,
issue_id=issue_id, issue_id=issue_id,
learn_more_url=learn_more_url, learn_more_url=learn_more_url,
severity=severity, severity=severity,
@ -81,6 +84,7 @@ class IssueRegistry:
issue, issue,
active=True, active=True,
breaks_in_ha_version=breaks_in_ha_version, breaks_in_ha_version=breaks_in_ha_version,
is_fixable=is_fixable,
learn_more_url=learn_more_url, learn_more_url=learn_more_url,
severity=severity, severity=severity,
translation_key=translation_key, translation_key=translation_key,
@ -127,6 +131,7 @@ class IssueRegistry:
breaks_in_ha_version=None, breaks_in_ha_version=None,
dismissed_version=issue["dismissed_version"], dismissed_version=issue["dismissed_version"],
domain=issue["domain"], domain=issue["domain"],
is_fixable=None,
issue_id=issue["issue_id"], issue_id=issue["issue_id"],
learn_more_url=None, learn_more_url=None,
severity=None, severity=None,

View File

@ -3,5 +3,6 @@
"name": "Resolution Center", "name": "Resolution Center",
"config_flow": false, "config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/resolution_center", "documentation": "https://www.home-assistant.io/integrations/resolution_center",
"codeowners": ["@home-assistant/core"] "codeowners": ["@home-assistant/core"],
"dependencies": ["http"]
} }

View File

@ -1,7 +1,11 @@
"""Models for Resolution Center.""" """Models for Resolution Center."""
from __future__ import annotations from __future__ import annotations
from typing import Protocol
from homeassistant import data_entry_flow
from homeassistant.backports.enum import StrEnum from homeassistant.backports.enum import StrEnum
from homeassistant.core import HomeAssistant
class IssueSeverity(StrEnum): class IssueSeverity(StrEnum):
@ -10,3 +14,16 @@ class IssueSeverity(StrEnum):
CRITICAL = "critical" CRITICAL = "critical"
ERROR = "error" ERROR = "error"
WARNING = "warning" WARNING = "warning"
class ResolutionCenterFlow(data_entry_flow.FlowHandler):
"""Handle a flow for fixing an issue."""
class ResolutionCenterProtocol(Protocol):
"""Define the format of resolution center platforms."""
async def async_create_fix_flow(
self, hass: HomeAssistant, issue_id: str
) -> ResolutionCenterFlow:
"""Create a flow to fix a fixable issue."""

View File

@ -2,13 +2,24 @@
from __future__ import annotations from __future__ import annotations
import dataclasses import dataclasses
from http import HTTPStatus
from typing import Any from typing import Any
from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.auth.permissions.const import POLICY_EDIT
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import Unauthorized
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView,
FlowManagerResourceView,
)
from .const import DOMAIN
from .issue_handler import async_dismiss_issue from .issue_handler import async_dismiss_issue
from .issue_registry import async_get as async_get_issue_registry from .issue_registry import async_get as async_get_issue_registry
@ -19,6 +30,13 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, ws_dismiss_issue) websocket_api.async_register_command(hass, ws_dismiss_issue)
websocket_api.async_register_command(hass, ws_list_issues) websocket_api.async_register_command(hass, ws_list_issues)
hass.http.register_view(
ResolutionCenterFlowIndexView(hass.data[DOMAIN]["flow_manager"])
)
hass.http.register_view(
ResolutionCenterFlowResourceView(hass.data[DOMAIN]["flow_manager"])
)
@callback @callback
@websocket_api.websocket_command( @websocket_api.websocket_command(
@ -60,3 +78,63 @@ def ws_list_issues(
] ]
connection.send_result(msg["id"], {"issues": issues}) connection.send_result(msg["id"], {"issues": issues})
class ResolutionCenterFlowIndexView(FlowManagerIndexView):
"""View to create issue fix flows."""
url = "/api/resolution_center/issues/fix"
name = "api:resolution_center:issues:fix"
@RequestDataValidator(
vol.Schema(
{
vol.Required("handler"): str,
vol.Required("issue_id"): str,
},
extra=vol.ALLOW_EXTRA,
)
)
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Handle a POST request."""
if not request["hass_user"].is_admin:
raise Unauthorized(permission=POLICY_EDIT)
try:
result = await self._flow_mgr.async_init(
data["handler"],
data={"issue_id": data["issue_id"]},
)
except data_entry_flow.UnknownHandler:
return self.json_message("Invalid handler specified", HTTPStatus.NOT_FOUND)
except data_entry_flow.UnknownStep:
return self.json_message(
"Handler does not support user", HTTPStatus.BAD_REQUEST
)
result = self._prepare_result_json(result)
return self.json(result) # pylint: disable=arguments-differ
class ResolutionCenterFlowResourceView(FlowManagerResourceView):
"""View to interact with the option flow manager."""
url = "/api/resolution_center/issues/fix/{flow_id}"
name = "api:resolution_center:issues:fix:resource"
async def get(self, request: web.Request, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
if not request["hass_user"].is_admin:
raise Unauthorized(permission=POLICY_EDIT)
return await super().get(request, flow_id)
# pylint: disable=arguments-differ
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
if not request["hass_user"].is_admin:
raise Unauthorized(permission=POLICY_EDIT)
# pylint: disable=no-value-for-parameter
return await super().post(request, flow_id) # type: ignore[no-any-return]

View File

@ -1,4 +1,6 @@
"""Test the resolution center websocket API.""" """Test the resolution center websocket API."""
from unittest.mock import AsyncMock, Mock
import pytest import pytest
from homeassistant.components.resolution_center import ( from homeassistant.components.resolution_center import (
@ -6,11 +8,16 @@ from homeassistant.components.resolution_center import (
async_delete_issue, async_delete_issue,
) )
from homeassistant.components.resolution_center.const import DOMAIN from homeassistant.components.resolution_center.const import DOMAIN
from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue from homeassistant.components.resolution_center.issue_handler import (
async_dismiss_issue,
async_process_resolution_center_platforms,
)
from homeassistant.const import __version__ as ha_version from homeassistant.const import __version__ as ha_version
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import mock_platform
async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
"""Test creating and updating issues.""" """Test creating and updating issues."""
@ -29,6 +36,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
"breaks_in_ha_version": "2022.9.0dev0", "breaks_in_ha_version": "2022.9.0dev0",
"domain": "test", "domain": "test",
"issue_id": "issue_1", "issue_id": "issue_1",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com", "learn_more_url": "https://theuselessweb.com",
"severity": "error", "severity": "error",
"translation_key": "abc_123", "translation_key": "abc_123",
@ -38,6 +46,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
"breaks_in_ha_version": "2022.8", "breaks_in_ha_version": "2022.8",
"domain": "test", "domain": "test",
"issue_id": "issue_2", "issue_id": "issue_2",
"is_fixable": False,
"learn_more_url": "https://theuselessweb.com/abc", "learn_more_url": "https://theuselessweb.com/abc",
"severity": "other", "severity": "other",
"translation_key": "even_worse", "translation_key": "even_worse",
@ -51,6 +60,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
issue["domain"], issue["domain"],
issue["issue_id"], issue["issue_id"],
breaks_in_ha_version=issue["breaks_in_ha_version"], breaks_in_ha_version=issue["breaks_in_ha_version"],
is_fixable=issue["is_fixable"],
learn_more_url=issue["learn_more_url"], learn_more_url=issue["learn_more_url"],
severity=issue["severity"], severity=issue["severity"],
translation_key=issue["translation_key"], translation_key=issue["translation_key"],
@ -78,6 +88,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
issues[0]["domain"], issues[0]["domain"],
issues[0]["issue_id"], issues[0]["issue_id"],
breaks_in_ha_version=issues[0]["breaks_in_ha_version"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
is_fixable=issues[0]["is_fixable"],
learn_more_url="blablabla", learn_more_url="blablabla",
severity=issues[0]["severity"], severity=issues[0]["severity"],
translation_key=issues[0]["translation_key"], translation_key=issues[0]["translation_key"],
@ -109,6 +120,7 @@ async def test_create_issue_invalid_version(
"breaks_in_ha_version": ha_version, "breaks_in_ha_version": ha_version,
"domain": "test", "domain": "test",
"issue_id": "issue_1", "issue_id": "issue_1",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com", "learn_more_url": "https://theuselessweb.com",
"severity": "error", "severity": "error",
"translation_key": "abc_123", "translation_key": "abc_123",
@ -121,6 +133,7 @@ async def test_create_issue_invalid_version(
issue["domain"], issue["domain"],
issue["issue_id"], issue["issue_id"],
breaks_in_ha_version=issue["breaks_in_ha_version"], breaks_in_ha_version=issue["breaks_in_ha_version"],
is_fixable=issue["is_fixable"],
learn_more_url=issue["learn_more_url"], learn_more_url=issue["learn_more_url"],
severity=issue["severity"], severity=issue["severity"],
translation_key=issue["translation_key"], translation_key=issue["translation_key"],
@ -150,6 +163,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
{ {
"breaks_in_ha_version": "2022.9", "breaks_in_ha_version": "2022.9",
"domain": "test", "domain": "test",
"is_fixable": True,
"issue_id": "issue_1", "issue_id": "issue_1",
"learn_more_url": "https://theuselessweb.com", "learn_more_url": "https://theuselessweb.com",
"severity": "error", "severity": "error",
@ -164,6 +178,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
issue["domain"], issue["domain"],
issue["issue_id"], issue["issue_id"],
breaks_in_ha_version=issue["breaks_in_ha_version"], breaks_in_ha_version=issue["breaks_in_ha_version"],
is_fixable=issue["is_fixable"],
learn_more_url=issue["learn_more_url"], learn_more_url=issue["learn_more_url"],
severity=issue["severity"], severity=issue["severity"],
translation_key=issue["translation_key"], translation_key=issue["translation_key"],
@ -246,6 +261,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
issues[0]["domain"], issues[0]["domain"],
issues[0]["issue_id"], issues[0]["issue_id"],
breaks_in_ha_version=issues[0]["breaks_in_ha_version"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
is_fixable=issues[0]["is_fixable"],
learn_more_url="blablabla", learn_more_url="blablabla",
severity=issues[0]["severity"], severity=issues[0]["severity"],
translation_key=issues[0]["translation_key"], translation_key=issues[0]["translation_key"],
@ -275,6 +291,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None:
"breaks_in_ha_version": "2022.9", "breaks_in_ha_version": "2022.9",
"domain": "fake_integration", "domain": "fake_integration",
"issue_id": "issue_1", "issue_id": "issue_1",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com", "learn_more_url": "https://theuselessweb.com",
"severity": "error", "severity": "error",
"translation_key": "abc_123", "translation_key": "abc_123",
@ -288,6 +305,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None:
issue["domain"], issue["domain"],
issue["issue_id"], issue["issue_id"],
breaks_in_ha_version=issue["breaks_in_ha_version"], breaks_in_ha_version=issue["breaks_in_ha_version"],
is_fixable=issue["is_fixable"],
learn_more_url=issue["learn_more_url"], learn_more_url=issue["learn_more_url"],
severity=issue["severity"], severity=issue["severity"],
translation_key=issue["translation_key"], translation_key=issue["translation_key"],
@ -344,3 +362,25 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None:
assert msg["success"] assert msg["success"]
assert msg["result"] == {"issues": []} assert msg["result"] == {"issues": []}
async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> None:
"""Test non-compliant platforms are not registered."""
hass.config.components.add("fake_integration")
hass.config.components.add("integration_without_diagnostics")
mock_platform(
hass,
"fake_integration.resolution_center",
Mock(async_create_fix_flow=AsyncMock(return_value=True)),
)
mock_platform(
hass,
"integration_without_diagnostics.resolution_center",
Mock(spec=[]),
)
assert await async_setup_component(hass, DOMAIN, {})
await async_process_resolution_center_platforms(hass)
assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"]

View File

@ -20,6 +20,7 @@ async def test_load_issues(hass: HomeAssistant) -> None:
"breaks_in_ha_version": "2022.9", "breaks_in_ha_version": "2022.9",
"domain": "test", "domain": "test",
"issue_id": "issue_1", "issue_id": "issue_1",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com", "learn_more_url": "https://theuselessweb.com",
"severity": "error", "severity": "error",
"translation_key": "abc_123", "translation_key": "abc_123",
@ -29,6 +30,7 @@ async def test_load_issues(hass: HomeAssistant) -> None:
"breaks_in_ha_version": "2022.8", "breaks_in_ha_version": "2022.8",
"domain": "test", "domain": "test",
"issue_id": "issue_2", "issue_id": "issue_2",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com/abc", "learn_more_url": "https://theuselessweb.com/abc",
"severity": "other", "severity": "other",
"translation_key": "even_worse", "translation_key": "even_worse",
@ -42,6 +44,7 @@ async def test_load_issues(hass: HomeAssistant) -> None:
issue["domain"], issue["domain"],
issue["issue_id"], issue["issue_id"],
breaks_in_ha_version=issue["breaks_in_ha_version"], breaks_in_ha_version=issue["breaks_in_ha_version"],
is_fixable=issue["is_fixable"],
learn_more_url=issue["learn_more_url"], learn_more_url=issue["learn_more_url"],
severity=issue["severity"], severity=issue["severity"],
translation_key=issue["translation_key"], translation_key=issue["translation_key"],

View File

@ -1,22 +1,33 @@
"""Test the resolution center websocket API.""" """Test the resolution center websocket API."""
from homeassistant.components.resolution_center import async_create_issue from __future__ import annotations
from http import HTTPStatus
from unittest.mock import ANY, AsyncMock, Mock
import pytest
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.resolution_center import (
ResolutionCenterFlow,
async_create_issue,
)
from homeassistant.components.resolution_center.const import DOMAIN from homeassistant.components.resolution_center.const import DOMAIN
from homeassistant.const import __version__ as ha_version from homeassistant.const import __version__ as ha_version
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import mock_platform
async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
"""Test we can dismiss an issue."""
assert await async_setup_component(hass, DOMAIN, {})
client = await hass_ws_client(hass)
async def create_issues(hass, ws_client):
"""Create issues."""
issues = [ issues = [
{ {
"breaks_in_ha_version": "2022.9", "breaks_in_ha_version": "2022.9",
"domain": "test", "domain": "fake_integration",
"issue_id": "issue_1", "issue_id": "issue_1",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com", "learn_more_url": "https://theuselessweb.com",
"severity": "error", "severity": "error",
"translation_key": "abc_123", "translation_key": "abc_123",
@ -30,14 +41,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
issue["domain"], issue["domain"],
issue["issue_id"], issue["issue_id"],
breaks_in_ha_version=issue["breaks_in_ha_version"], breaks_in_ha_version=issue["breaks_in_ha_version"],
is_fixable=issue["is_fixable"],
learn_more_url=issue["learn_more_url"], learn_more_url=issue["learn_more_url"],
severity=issue["severity"], severity=issue["severity"],
translation_key=issue["translation_key"], translation_key=issue["translation_key"],
translation_placeholders=issue["translation_placeholders"], translation_placeholders=issue["translation_placeholders"],
) )
await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) await ws_client.send_json({"id": 1, "type": "resolution_center/list_issues"})
msg = await client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert msg["result"] == { assert msg["result"] == {
@ -51,11 +63,63 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
] ]
} }
return issues
class MockFixFlow(ResolutionCenterFlow):
"""Handler for an issue fixing flow."""
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
return await (self.async_step_confirm())
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
return self.async_create_entry(title=None, data=None)
return self.async_show_form(step_id="confirm", data_schema=vol.Schema({}))
@pytest.fixture(autouse=True)
async def mock_resolution_center_integration(hass):
"""Mock a resolution_center integration."""
hass.config.components.add("fake_integration")
hass.config.components.add("integration_without_diagnostics")
def async_create_fix_flow(hass, issue_id):
return MockFixFlow()
mock_platform(
hass,
"fake_integration.resolution_center",
Mock(async_create_fix_flow=AsyncMock(wraps=async_create_fix_flow)),
)
mock_platform(
hass,
"integration_without_diagnostics.resolution_center",
Mock(spec=[]),
)
async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
"""Test we can dismiss an issue."""
assert await async_setup_component(hass, DOMAIN, {})
client = await hass_ws_client(hass)
issues = await create_issues(hass, client)
await client.send_json( await client.send_json(
{ {
"id": 2, "id": 2,
"type": "resolution_center/dismiss_issue", "type": "resolution_center/dismiss_issue",
"domain": "test", "domain": "fake_integration",
"issue_id": "no_such_issue", "issue_id": "no_such_issue",
} }
) )
@ -66,7 +130,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
{ {
"id": 3, "id": 3,
"type": "resolution_center/dismiss_issue", "type": "resolution_center/dismiss_issue",
"domain": "test", "domain": "fake_integration",
"issue_id": "issue_1", "issue_id": "issue_1",
} }
) )
@ -90,6 +154,185 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
} }
async def test_fix_non_existing_issue(
hass: HomeAssistant, hass_client, hass_ws_client
) -> None:
"""Test trying to fix an issue that doesn't exist."""
assert await async_setup_component(hass, "http", {})
assert await async_setup_component(hass, DOMAIN, {})
ws_client = await hass_ws_client(hass)
client = await hass_client()
issues = await create_issues(hass, ws_client)
url = "/api/resolution_center/issues/fix"
resp = await client.post(
url, json={"handler": "no_such_integration", "issue_id": "no_such_issue"}
)
assert resp.status != HTTPStatus.OK
url = "/api/resolution_center/issues/fix"
resp = await client.post(
url, json={"handler": "fake_integration", "issue_id": "no_such_issue"}
)
assert resp.status != HTTPStatus.OK
await ws_client.send_json({"id": 3, "type": "resolution_center/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert msg["result"] == {
"issues": [
dict(
issue,
dismissed=False,
dismissed_version=None,
)
for issue in issues
]
}
async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None:
"""Test we can fix an issue."""
assert await async_setup_component(hass, "http", {})
assert await async_setup_component(hass, DOMAIN, {})
ws_client = await hass_ws_client(hass)
client = await hass_client()
await create_issues(hass, ws_client)
url = "/api/resolution_center/issues/fix"
resp = await client.post(
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data == {
"data_schema": [],
"description_placeholders": None,
"errors": None,
"flow_id": ANY,
"handler": "fake_integration",
"last_step": None,
"step_id": "confirm",
"type": "form",
}
url = f"/api/resolution_center/issues/fix/{flow_id}"
# Test we can get the status of the flow
resp2 = await client.get(url)
assert resp2.status == HTTPStatus.OK
data2 = await resp2.json()
assert data == data2
resp = await client.post(url)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data == {
"description": None,
"description_placeholders": None,
"flow_id": flow_id,
"handler": "fake_integration",
"title": None,
"type": "create_entry",
"version": 1,
}
await ws_client.send_json({"id": 4, "type": "resolution_center/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert msg["result"] == {"issues": []}
async def test_fix_issue_unauth(
hass: HomeAssistant, hass_client, hass_admin_user
) -> None:
"""Test we can't query the result if not authorized."""
assert await async_setup_component(hass, "http", {})
assert await async_setup_component(hass, DOMAIN, {})
hass_admin_user.groups = []
client = await hass_client()
url = "/api/resolution_center/issues/fix"
resp = await client.post(
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
)
assert resp.status == HTTPStatus.UNAUTHORIZED
async def test_get_progress_unauth(
hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user
) -> None:
"""Test we can't fix an issue if not authorized."""
assert await async_setup_component(hass, "http", {})
assert await async_setup_component(hass, DOMAIN, {})
ws_client = await hass_ws_client(hass)
client = await hass_client()
await create_issues(hass, ws_client)
url = "/api/resolution_center/issues/fix"
resp = await client.post(
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
hass_admin_user.groups = []
url = f"/api/resolution_center/issues/fix/{flow_id}"
# Test we can't get the status of the flow
resp = await client.get(url)
assert resp.status == HTTPStatus.UNAUTHORIZED
async def test_step_unauth(
hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user
) -> None:
"""Test we can't fix an issue if not authorized."""
assert await async_setup_component(hass, "http", {})
assert await async_setup_component(hass, DOMAIN, {})
ws_client = await hass_ws_client(hass)
client = await hass_client()
await create_issues(hass, ws_client)
url = "/api/resolution_center/issues/fix"
resp = await client.post(
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
hass_admin_user.groups = []
url = f"/api/resolution_center/issues/fix/{flow_id}"
# Test we can't get the status of the flow
resp = await client.post(url)
assert resp.status == HTTPStatus.UNAUTHORIZED
async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
"""Test we can list issues.""" """Test we can list issues."""
assert await async_setup_component(hass, DOMAIN, {}) assert await async_setup_component(hass, DOMAIN, {})
@ -106,6 +349,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
{ {
"breaks_in_ha_version": "2022.9", "breaks_in_ha_version": "2022.9",
"domain": "test", "domain": "test",
"is_fixable": True,
"issue_id": "issue_1", "issue_id": "issue_1",
"learn_more_url": "https://theuselessweb.com", "learn_more_url": "https://theuselessweb.com",
"severity": "error", "severity": "error",
@ -115,6 +359,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
{ {
"breaks_in_ha_version": "2022.8", "breaks_in_ha_version": "2022.8",
"domain": "test", "domain": "test",
"is_fixable": False,
"issue_id": "issue_2", "issue_id": "issue_2",
"learn_more_url": "https://theuselessweb.com/abc", "learn_more_url": "https://theuselessweb.com/abc",
"severity": "other", "severity": "other",
@ -129,6 +374,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
issue["domain"], issue["domain"],
issue["issue_id"], issue["issue_id"],
breaks_in_ha_version=issue["breaks_in_ha_version"], breaks_in_ha_version=issue["breaks_in_ha_version"],
is_fixable=issue["is_fixable"],
learn_more_url=issue["learn_more_url"], learn_more_url=issue["learn_more_url"],
severity=issue["severity"], severity=issue["severity"],
translation_key=issue["translation_key"], translation_key=issue["translation_key"],