From 0e3f7bc63aecc71685536f2121d3fecbc24bd642 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Jul 2022 05:49:07 +0200 Subject: [PATCH] Resolution center MVP (#74243) Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/resolution_center/__init__.py | 20 + .../components/resolution_center/const.py | 3 + .../resolution_center/issue_handler.py | 62 ++++ .../resolution_center/issue_registry.py | 164 +++++++++ .../resolution_center/manifest.json | 7 + .../components/resolution_center/models.py | 12 + .../resolution_center/websocket_api.py | 62 ++++ mypy.ini | 11 + script/hassfest/manifest.py | 1 + .../components/resolution_center/__init__.py | 1 + .../components/resolution_center/test_init.py | 346 ++++++++++++++++++ .../resolution_center/test_issue_registry.py | 92 +++++ .../resolution_center/test_websocket_api.py | 151 ++++++++ 15 files changed, 935 insertions(+) create mode 100644 homeassistant/components/resolution_center/__init__.py create mode 100644 homeassistant/components/resolution_center/const.py create mode 100644 homeassistant/components/resolution_center/issue_handler.py create mode 100644 homeassistant/components/resolution_center/issue_registry.py create mode 100644 homeassistant/components/resolution_center/manifest.json create mode 100644 homeassistant/components/resolution_center/models.py create mode 100644 homeassistant/components/resolution_center/websocket_api.py create mode 100644 tests/components/resolution_center/__init__.py create mode 100644 tests/components/resolution_center/test_init.py create mode 100644 tests/components/resolution_center/test_issue_registry.py create mode 100644 tests/components/resolution_center/test_websocket_api.py diff --git a/.strict-typing b/.strict-typing index ebfed5dfa5bb..c5ed000a7ceb 100644 --- a/.strict-typing +++ b/.strict-typing @@ -191,6 +191,7 @@ homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* +homeassistant.components.resolution_center.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* diff --git a/CODEOWNERS b/CODEOWNERS index 9f53aeab34e0..fda94805214b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -857,6 +857,8 @@ build.json @home-assistant/supervisor /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet /homeassistant/components/repetier/ @MTrab @ShadowBr0ther +/homeassistant/components/resolution_center/ @home-assistant/core +/tests/components/resolution_center/ @home-assistant/core /homeassistant/components/rflink/ @javicalle /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/resolution_center/__init__.py new file mode 100644 index 000000000000..1446aa68bbae --- /dev/null +++ b/homeassistant/components/resolution_center/__init__.py @@ -0,0 +1,20 @@ +"""The resolution center integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from . import websocket_api +from .const import DOMAIN +from .issue_handler import 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"] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Resolution Center.""" + websocket_api.async_setup(hass) + await async_load_issue_registry(hass) + + return True diff --git a/homeassistant/components/resolution_center/const.py b/homeassistant/components/resolution_center/const.py new file mode 100644 index 000000000000..46d020e5118c --- /dev/null +++ b/homeassistant/components/resolution_center/const.py @@ -0,0 +1,3 @@ +"""Constants for the Resolution Center integration.""" + +DOMAIN = "resolution_center" diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py new file mode 100644 index 000000000000..245895fa2dbc --- /dev/null +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -0,0 +1,62 @@ +"""The resolution center integration.""" +from __future__ import annotations + +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.core import HomeAssistant, callback + +from .issue_registry import async_get as async_get_issue_registry +from .models import IssueSeverity + + +@callback +def async_create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + # Verify the breaks_in_ha_version is a valid version string + if breaks_in_ha_version: + AwesomeVersion( + breaks_in_ha_version, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + issue_registry = async_get_issue_registry(hass) + issue_registry.async_get_or_create( + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + +@callback +def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_delete(domain, issue_id) + + +@callback +def async_dismiss_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Dismiss an issue. + + Will raise if the issue does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_dismiss(domain, issue_id) diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py new file mode 100644 index 000000000000..d97ad73bbacc --- /dev/null +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -0,0 +1,164 @@ +"""Persistently store issues raised by integrations.""" +from __future__ import annotations + +import dataclasses +from typing import cast + +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store + +from .models import IssueSeverity + +DATA_REGISTRY = "issue_registry" +STORAGE_KEY = "resolution_center.issue_registry" +STORAGE_VERSION = 1 +SAVE_DELAY = 10 +SAVED_FIELDS = ("dismissed_version", "domain", "issue_id") + + +@dataclasses.dataclass(frozen=True) +class IssueEntry: + """Issue Registry Entry.""" + + active: bool + breaks_in_ha_version: str | None + dismissed_version: str | None + domain: str + issue_id: str + learn_more_url: str | None + severity: IssueSeverity | None + translation_key: str | None + translation_placeholders: dict[str, str] | None + + +class IssueRegistry: + """Class to hold a registry of issues.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the issue registry.""" + self.hass = hass + self.issues: dict[tuple[str, str], IssueEntry] = {} + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + + @callback + def async_get_issue(self, domain: str, issue_id: str) -> IssueEntry | None: + """Get issue by id.""" + return self.issues.get((domain, issue_id)) + + @callback + def async_get_or_create( + self, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, + ) -> IssueEntry: + """Get issue. Create if it doesn't exist.""" + + if (issue := self.async_get_issue(domain, issue_id)) is None: + issue = IssueEntry( + active=True, + breaks_in_ha_version=breaks_in_ha_version, + dismissed_version=None, + domain=domain, + issue_id=issue_id, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.issues[(domain, issue_id)] = issue + self.async_schedule_save() + else: + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + issue, + active=True, + breaks_in_ha_version=breaks_in_ha_version, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + return issue + + @callback + def async_delete(self, domain: str, issue_id: str) -> None: + """Delete issue.""" + if self.issues.pop((domain, issue_id), None) is None: + return + + self.async_schedule_save() + + @callback + def async_dismiss(self, domain: str, issue_id: str) -> IssueEntry: + """Dismiss issue.""" + old = self.issues[(domain, issue_id)] + if old.dismissed_version == ha_version: + return old + + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + old, + dismissed_version=ha_version, + ) + + self.async_schedule_save() + + return issue + + async def async_load(self) -> None: + """Load the issue registry.""" + data = await self._store.async_load() + + issues: dict[tuple[str, str], IssueEntry] = {} + + if isinstance(data, dict): + for issue in data["issues"]: + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=False, + breaks_in_ha_version=None, + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + issue_id=issue["issue_id"], + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) + + self.issues = issues + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the issue registry.""" + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: + """Return data of issue registry to store in a file.""" + data = {} + + data["issues"] = [ + {field: getattr(entry, field) for field in SAVED_FIELDS} + for entry in self.issues.values() + ] + + return data + + +@callback +def async_get(hass: HomeAssistant) -> IssueRegistry: + """Get issue registry.""" + return cast(IssueRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistant) -> None: + """Load issue registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = IssueRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/resolution_center/manifest.json new file mode 100644 index 000000000000..87cd309ad3df --- /dev/null +++ b/homeassistant/components/resolution_center/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "resolution_center", + "name": "Resolution Center", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/resolution_center", + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/resolution_center/models.py new file mode 100644 index 000000000000..eabfd98cef3f --- /dev/null +++ b/homeassistant/components/resolution_center/models.py @@ -0,0 +1,12 @@ +"""Models for Resolution Center.""" +from __future__ import annotations + +from homeassistant.backports.enum import StrEnum + + +class IssueSeverity(StrEnum): + """Issue severity.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py new file mode 100644 index 000000000000..14793f0bd2db --- /dev/null +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -0,0 +1,62 @@ +"""The resolution center websocket API.""" +from __future__ import annotations + +import dataclasses +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .issue_handler import async_dismiss_issue +from .issue_registry import async_get as async_get_issue_registry + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the resolution center websocket API.""" + websocket_api.async_register_command(hass, ws_dismiss_issue) + websocket_api.async_register_command(hass, ws_list_issues) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "resolution_center/dismiss_issue", + vol.Required("domain"): str, + vol.Required("issue_id"): str, + } +) +def ws_dismiss_issue( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fix an issue.""" + async_dismiss_issue(hass, msg["domain"], msg["issue_id"]) + + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "resolution_center/list_issues", + } +) +@callback +def ws_list_issues( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return a list of issues.""" + + def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: + result = {k: v for k, v in kv_pairs if k != "active"} + result["dismissed"] = result["dismissed_version"] is not None + return result + + issue_registry = async_get_issue_registry(hass) + issues = [ + dataclasses.asdict(issue, dict_factory=ws_dict) + for issue in issue_registry.issues.values() + ] + + connection.send_result(msg["id"], {"issues": issues}) diff --git a/mypy.ini b/mypy.ini index 87dd265eeef6..c47413b4af87 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1864,6 +1864,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.resolution_center.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ridwell.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 0cd203645331..b847d3843616 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -80,6 +80,7 @@ NO_IOT_CLASS = [ "proxy", "python_script", "raspberry_pi", + "resolution_center", "safe_mode", "script", "search", diff --git a/tests/components/resolution_center/__init__.py b/tests/components/resolution_center/__init__.py new file mode 100644 index 000000000000..a6a86bf99bc9 --- /dev/null +++ b/tests/components/resolution_center/__init__.py @@ -0,0 +1 @@ +"""Tests for the resolution center integration.""" diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py new file mode 100644 index 000000000000..869c5d6c4852 --- /dev/null +++ b/tests/components/resolution_center/test_init.py @@ -0,0 +1,346 @@ +"""Test the resolution center websocket API.""" +import pytest + +from homeassistant.components.resolution_center import ( + async_create_issue, + async_delete_issue, +) +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test creating and updating issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9.0dev0", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + 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": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Update an issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + dismissed=False, + dismissed_version=None, + learn_more_url="blablabla", + ) + + +@pytest.mark.parametrize("ha_version", ("2022.9.cat", "In the future: 2023.1.1")) +async def test_create_issue_invalid_version( + hass: HomeAssistant, hass_ws_client, ha_version +) -> None: + """Test creating an issue with invalid breaks in version.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issue = { + "breaks_in_ha_version": ha_version, + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } + + with pytest.raises(Exception): + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + 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() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test dismissing issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + 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": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Dismiss a non-existing issue + with pytest.raises(KeyError): + async_dismiss_issue(hass, issues[0]["domain"], "no_such_issue") + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Dismiss an existing issue + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + # Dismiss the same issue again + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + # Update a dismissed issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + dismissed=True, + dismissed_version=ha_version, + learn_more_url="blablabla", + ) + + +async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can delete an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "fake_integration", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + 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() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Delete a non-existing issue + async_delete_issue(hass, issues[0]["domain"], "no_such_issue") + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Delete an existing issue + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + # Delete the same issue again + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py new file mode 100644 index 000000000000..c236e96adb26 --- /dev/null +++ b/tests/components/resolution_center/test_issue_registry.py @@ -0,0 +1,92 @@ +"""Test the resolution center websocket API.""" +from homeassistant.components.resolution_center import ( + async_create_issue, + issue_registry, +) +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import flush_store + + +async def test_load_issues(hass: HomeAssistant) -> None: + """Make sure that we can load/save data correctly.""" + assert await async_setup_component(hass, DOMAIN, {}) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 + issue1 = registry.async_get_issue("test", "issue_1") + issue2 = registry.async_get_issue("test", "issue_2") + + registry2 = issue_registry.IssueRegistry(hass) + await flush_store(registry._store) + await registry2.async_load() + + assert list(registry.issues) == list(registry2.issues) + + issue1_registry2 = registry2.async_get_issue("test", "issue_1") + assert issue1_registry2.dismissed_version == issue1.dismissed_version + issue2_registry2 = registry2.async_get_issue("test", "issue_2") + assert issue2_registry2.dismissed_version == issue2.dismissed_version + + +async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: + """Test loading stored issues on start.""" + hass_storage[issue_registry.STORAGE_KEY] = { + "version": issue_registry.STORAGE_VERSION, + "data": { + "issues": [ + { + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "issue_id": "issue_1", + }, + { + "dismissed_version": None, + "domain": "test", + "issue_id": "issue_2", + }, + ] + }, + } + + assert await async_setup_component(hass, DOMAIN, {}) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py new file mode 100644 index 000000000000..9258a06f9046 --- /dev/null +++ b/tests/components/resolution_center/test_websocket_api.py @@ -0,0 +1,151 @@ +"""Test the resolution center websocket API.""" +from homeassistant.components.resolution_center import 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 + + +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 = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + 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() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + await client.send_json( + { + "id": 2, + "type": "resolution_center/dismiss_issue", + "domain": "test", + "issue_id": "no_such_issue", + } + ) + msg = await client.receive_json() + assert not msg["success"] + + await client.send_json( + { + "id": 3, + "type": "resolution_center/dismiss_issue", + "domain": "test", + "issue_id": "issue_1", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + +async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can list issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + 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": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + }