From b0fde206b8b25f9fb7e7e24d07849c2b6b76ee83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Jul 2022 19:26:06 +0200 Subject: [PATCH] Teach resolution center about fixing issues (#74694) --- .../components/resolution_center/__init__.py | 9 +- .../resolution_center/issue_handler.py | 77 ++++- .../resolution_center/issue_registry.py | 5 + .../resolution_center/manifest.json | 3 +- .../components/resolution_center/models.py | 17 ++ .../resolution_center/websocket_api.py | 78 +++++ .../components/resolution_center/test_init.py | 42 ++- .../resolution_center/test_issue_registry.py | 3 + .../resolution_center/test_websocket_api.py | 268 +++++++++++++++++- 9 files changed, 485 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/resolution_center/__init__.py index 1446aa68bba..7d0cd8416c3 100644 --- a/homeassistant/components/resolution_center/__init__.py +++ b/homeassistant/components/resolution_center/__init__.py @@ -4,16 +4,19 @@ from __future__ import annotations from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import websocket_api +from . import issue_handler, websocket_api 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 -__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: """Set up Resolution Center.""" + hass.data[DOMAIN] = {} + + issue_handler.async_setup(hass) websocket_api.async_setup(hass) await async_load_issue_registry(hass) diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py index 245895fa2db..210a998ce14 100644 --- a/homeassistant/components/resolution_center/issue_handler.py +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -1,12 +1,85 @@ """The resolution center integration.""" from __future__ import annotations +from typing import Any + from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +from homeassistant import data_entry_flow 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 .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 @@ -16,6 +89,7 @@ def async_create_issue( issue_id: str, *, breaks_in_ha_version: str | None = None, + is_fixable: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -35,6 +109,7 @@ def async_create_issue( domain, issue_id, breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index 7d5bbb482ba..d9042d891dd 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -25,6 +25,7 @@ class IssueEntry: breaks_in_ha_version: str | None dismissed_version: str | None domain: str + is_fixable: bool | None issue_id: str learn_more_url: str | None severity: IssueSeverity | None @@ -55,6 +56,7 @@ class IssueRegistry: issue_id: str, *, breaks_in_ha_version: str | None = None, + is_fixable: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -68,6 +70,7 @@ class IssueRegistry: breaks_in_ha_version=breaks_in_ha_version, dismissed_version=None, domain=domain, + is_fixable=is_fixable, issue_id=issue_id, learn_more_url=learn_more_url, severity=severity, @@ -81,6 +84,7 @@ class IssueRegistry: issue, active=True, breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -127,6 +131,7 @@ class IssueRegistry: breaks_in_ha_version=None, dismissed_version=issue["dismissed_version"], domain=issue["domain"], + is_fixable=None, issue_id=issue["issue_id"], learn_more_url=None, severity=None, diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/resolution_center/manifest.json index 87cd309ad3d..4b8c1bb2506 100644 --- a/homeassistant/components/resolution_center/manifest.json +++ b/homeassistant/components/resolution_center/manifest.json @@ -3,5 +3,6 @@ "name": "Resolution Center", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/resolution_center", - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "dependencies": ["http"] } diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/resolution_center/models.py index eabfd98cef3..12e2f4e73f3 100644 --- a/homeassistant/components/resolution_center/models.py +++ b/homeassistant/components/resolution_center/models.py @@ -1,7 +1,11 @@ """Models for Resolution Center.""" from __future__ import annotations +from typing import Protocol + +from homeassistant import data_entry_flow from homeassistant.backports.enum import StrEnum +from homeassistant.core import HomeAssistant class IssueSeverity(StrEnum): @@ -10,3 +14,16 @@ class IssueSeverity(StrEnum): CRITICAL = "critical" ERROR = "error" 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.""" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py index 14793f0bd2d..dfa4f1903d9 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -2,13 +2,24 @@ from __future__ import annotations import dataclasses +from http import HTTPStatus from typing import Any +from aiohttp import web 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.http.data_validator import RequestDataValidator 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_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_list_issues) + hass.http.register_view( + ResolutionCenterFlowIndexView(hass.data[DOMAIN]["flow_manager"]) + ) + hass.http.register_view( + ResolutionCenterFlowResourceView(hass.data[DOMAIN]["flow_manager"]) + ) + @callback @websocket_api.websocket_command( @@ -60,3 +78,63 @@ def ws_list_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] diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py index 869c5d6c485..66f9bc42935 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/resolution_center/test_init.py @@ -1,4 +1,6 @@ """Test the resolution center websocket API.""" +from unittest.mock import AsyncMock, Mock + import pytest from homeassistant.components.resolution_center import ( @@ -6,11 +8,16 @@ from homeassistant.components.resolution_center import ( async_delete_issue, ) 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.core import HomeAssistant 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: """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", "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "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", "domain": "test", "issue_id": "issue_2", + "is_fixable": False, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -51,6 +60,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], 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]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -109,6 +120,7 @@ async def test_create_issue_invalid_version( "breaks_in_ha_version": ha_version, "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -121,6 +133,7 @@ async def test_create_issue_invalid_version( issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], 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", "domain": "test", + "is_fixable": True, "issue_id": "issue_1", "learn_more_url": "https://theuselessweb.com", "severity": "error", @@ -164,6 +178,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], 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]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], learn_more_url="blablabla", severity=issues[0]["severity"], 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", "domain": "fake_integration", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -288,6 +305,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], 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["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"] diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py index c236e96adb2..a0dffaacc8f 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/resolution_center/test_issue_registry.py @@ -20,6 +20,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "breaks_in_ha_version": "2022.9", "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -29,6 +30,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "breaks_in_ha_version": "2022.8", "domain": "test", "issue_id": "issue_2", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -42,6 +44,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py index 9258a06f904..42899065121 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/resolution_center/test_websocket_api.py @@ -1,22 +1,33 @@ """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.const import __version__ as ha_version from homeassistant.core import HomeAssistant 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 = [ { "breaks_in_ha_version": "2022.9", - "domain": "test", + "domain": "fake_integration", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -30,14 +41,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) - msg = await client.receive_json() + await ws_client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await ws_client.receive_json() assert msg["success"] 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( { "id": 2, "type": "resolution_center/dismiss_issue", - "domain": "test", + "domain": "fake_integration", "issue_id": "no_such_issue", } ) @@ -66,7 +130,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: { "id": 3, "type": "resolution_center/dismiss_issue", - "domain": "test", + "domain": "fake_integration", "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: """Test we can list issues.""" 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", "domain": "test", + "is_fixable": True, "issue_id": "issue_1", "learn_more_url": "https://theuselessweb.com", "severity": "error", @@ -115,6 +359,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: { "breaks_in_ha_version": "2022.8", "domain": "test", + "is_fixable": False, "issue_id": "issue_2", "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", @@ -129,6 +374,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"],