Create issues for reauth flows (#109105)
parent
71c2460161
commit
ffdcdaf43b
|
@ -46,6 +46,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_entry_reauth": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Reauthentication is needed"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
|
|
|
@ -29,6 +29,7 @@ from .const import DOMAIN
|
|||
@callback
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the repairs websocket API."""
|
||||
websocket_api.async_register_command(hass, ws_get_issue_data)
|
||||
websocket_api.async_register_command(hass, ws_ignore_issue)
|
||||
websocket_api.async_register_command(hass, ws_list_issues)
|
||||
|
||||
|
@ -36,6 +37,29 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||
hass.http.register_view(RepairsFlowResourceView(hass.data[DOMAIN]["flow_manager"]))
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "repairs/get_issue_data",
|
||||
vol.Required("domain"): str,
|
||||
vol.Required("issue_id"): str,
|
||||
}
|
||||
)
|
||||
def ws_get_issue_data(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Fix an issue."""
|
||||
issue_registry = async_get_issue_registry(hass)
|
||||
if not (issue := issue_registry.async_get_issue(msg["domain"], msg["issue_id"])):
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
"unknown_issue",
|
||||
f"Issue '{msg['issue_id']}' not found",
|
||||
)
|
||||
return
|
||||
connection.send_result(msg["id"], {"issue_data": issue.data})
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
|
|
|
@ -23,15 +23,23 @@ from typing import TYPE_CHECKING, Any, Self, TypeVar, cast
|
|||
from . import data_entry_flow, loader
|
||||
from .components import persistent_notification
|
||||
from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from .core import CALLBACK_TYPE, CoreState, Event, HassJob, HomeAssistant, callback
|
||||
from .data_entry_flow import FlowResult
|
||||
from .core import (
|
||||
CALLBACK_TYPE,
|
||||
DOMAIN as HA_DOMAIN,
|
||||
CoreState,
|
||||
Event,
|
||||
HassJob,
|
||||
HomeAssistant,
|
||||
callback,
|
||||
)
|
||||
from .data_entry_flow import FLOW_NOT_COMPLETE_STEPS, FlowResult
|
||||
from .exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from .helpers import device_registry, entity_registry, storage
|
||||
from .helpers import device_registry, entity_registry, issue_registry as ir, storage
|
||||
from .helpers.debounce import Debouncer
|
||||
from .helpers.dispatcher import async_dispatcher_send
|
||||
from .helpers.event import (
|
||||
|
@ -793,7 +801,7 @@ class ConfigEntry:
|
|||
if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})):
|
||||
# Reauth flow already in progress for this entry
|
||||
return
|
||||
await hass.config_entries.flow.async_init(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
self.domain,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
|
@ -804,6 +812,21 @@ class ConfigEntry:
|
|||
| (context or {}),
|
||||
data=self.data | (data or {}),
|
||||
)
|
||||
if result["type"] not in FLOW_NOT_COMPLETE_STEPS:
|
||||
return
|
||||
|
||||
# Create an issue, there's no need to hold the lock when doing that
|
||||
issue_id = f"config_entry_reauth_{self.domain}_{self.entry_id}"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HA_DOMAIN,
|
||||
issue_id,
|
||||
data={"flow_id": result["flow_id"]},
|
||||
is_fixable=False,
|
||||
issue_domain=self.domain,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="config_entry_reauth",
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_get_active_flows(
|
||||
|
@ -981,6 +1004,14 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
|
|||
if not self._async_has_other_discovery_flows(flow.flow_id):
|
||||
persistent_notification.async_dismiss(self.hass, DISCOVERY_NOTIFICATION_ID)
|
||||
|
||||
# Clean up issue if this is a reauth flow
|
||||
if flow.context["source"] == SOURCE_REAUTH:
|
||||
if (entry_id := flow.context.get("entry_id")) is not None and (
|
||||
entry := self.config_entries.async_get_entry(entry_id)
|
||||
) is not None:
|
||||
issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}"
|
||||
ir.async_delete_issue(self.hass, HA_DOMAIN, issue_id)
|
||||
|
||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return result
|
||||
|
||||
|
@ -1230,12 +1261,15 @@ class ConfigEntries:
|
|||
ent_reg.async_clear_config_entry(entry_id)
|
||||
|
||||
# If the configuration entry is removed during reauth, it should
|
||||
# abort any reauth flow that is active for the removed entry.
|
||||
# abort any reauth flow that is active for the removed entry and
|
||||
# linked issues.
|
||||
for progress_flow in self.hass.config_entries.flow.async_progress_by_handler(
|
||||
entry.domain, match_context={"entry_id": entry_id, "source": SOURCE_REAUTH}
|
||||
):
|
||||
if "flow_id" in progress_flow:
|
||||
self.hass.config_entries.flow.async_abort(progress_flow["flow_id"])
|
||||
issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}"
|
||||
ir.async_delete_issue(self.hass, HA_DOMAIN, issue_id)
|
||||
|
||||
# After we have fully removed an "ignore" config entry we can try and rediscover
|
||||
# it so that a user is able to immediately start configuring it. We do this by
|
||||
|
|
|
@ -79,6 +79,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None:
|
|||
"homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth",
|
||||
return_value={
|
||||
"type": data_entry_flow.FlowResultType.FORM,
|
||||
"flow_id": "mock_flow",
|
||||
"step_id": "reauth_confirm",
|
||||
},
|
||||
) as mock_async_step_reauth:
|
||||
|
|
|
@ -25,6 +25,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None:
|
|||
"homeassistant.components.aussie_broadband.config_flow.ConfigFlow.async_step_reauth",
|
||||
return_value={
|
||||
"type": data_entry_flow.FlowResultType.FORM,
|
||||
"flow_id": "mock_flow",
|
||||
"step_id": "reauth_confirm",
|
||||
},
|
||||
) as mock_async_step_reauth:
|
||||
|
|
|
@ -579,3 +579,78 @@ async def test_fix_issue_aborted(
|
|||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
assert msg["result"]["issues"][0] == first_issue
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2022-07-19 07:53:05")
|
||||
async def test_get_issue_data(hass: HomeAssistant, hass_ws_client) -> None:
|
||||
"""Test we can get issue data."""
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
issues = [
|
||||
{
|
||||
"breaks_in_ha_version": "2022.9",
|
||||
"data": None,
|
||||
"domain": "test",
|
||||
"is_fixable": True,
|
||||
"issue_id": "issue_1",
|
||||
"issue_domain": None,
|
||||
"learn_more_url": "https://theuselessweb.com",
|
||||
"severity": "error",
|
||||
"translation_key": "abc_123",
|
||||
"translation_placeholders": {"abc": "123"},
|
||||
},
|
||||
{
|
||||
"breaks_in_ha_version": "2022.8",
|
||||
"data": {"key": "value"},
|
||||
"domain": "test",
|
||||
"is_fixable": False,
|
||||
"issue_id": "issue_2",
|
||||
"issue_domain": None,
|
||||
"learn_more_url": "https://theuselessweb.com/abc",
|
||||
"severity": "other",
|
||||
"translation_key": "even_worse",
|
||||
"translation_placeholders": {"def": "456"},
|
||||
},
|
||||
]
|
||||
|
||||
for issue in issues:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
issue["domain"],
|
||||
issue["issue_id"],
|
||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||
data=issue["data"],
|
||||
is_fixable=issue["is_fixable"],
|
||||
is_persistent=False,
|
||||
learn_more_url=issue["learn_more_url"],
|
||||
severity=issue["severity"],
|
||||
translation_key=issue["translation_key"],
|
||||
translation_placeholders=issue["translation_placeholders"],
|
||||
)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{"type": "repairs/get_issue_data", "domain": "test", "issue_id": "issue_1"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issue_data": None}
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{"type": "repairs/get_issue_data", "domain": "test", "issue_id": "issue_2"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issue_data": {"key": "value"}}
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{"type": "repairs/get_issue_data", "domain": "test", "issue_id": "unknown"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert not msg["success"]
|
||||
assert msg["error"] == {
|
||||
"code": "unknown_issue",
|
||||
"message": "Issue 'unknown' not found",
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ async def test_reauth_triggered(hass: HomeAssistant) -> None:
|
|||
"homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth",
|
||||
return_value={
|
||||
"type": data_entry_flow.FlowResultType.FORM,
|
||||
"flow_id": "mock_flow",
|
||||
"step_id": "reauth_confirm",
|
||||
},
|
||||
) as mock_async_step_reauth:
|
||||
|
|
|
@ -6,7 +6,7 @@ from collections.abc import Generator
|
|||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
@ -19,7 +19,13 @@ from homeassistant.const import (
|
|||
EVENT_HOMEASSISTANT_STARTED,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
CoreState,
|
||||
Event,
|
||||
HomeAssistant,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
|
@ -27,7 +33,7 @@ from homeassistant.exceptions import (
|
|||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
@ -59,7 +65,13 @@ def mock_handlers() -> Generator[None, None, None]:
|
|||
|
||||
async def async_step_reauth(self, data):
|
||||
"""Mock Reauth."""
|
||||
return self.async_show_form(step_id="reauth")
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input=None):
|
||||
"""Test reauth confirm step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return self.async_abort(reason="test")
|
||||
|
||||
with patch.dict(
|
||||
config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler}
|
||||
|
@ -425,10 +437,15 @@ async def test_remove_entry_cancels_reauth(
|
|||
assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH
|
||||
assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue_id = f"config_entry_reauth_test_{entry.entry_id}"
|
||||
assert issue_registry.async_get_issue(HA_DOMAIN, issue_id)
|
||||
|
||||
await manager.async_remove(entry.entry_id)
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler("test")
|
||||
assert len(flows) == 0
|
||||
assert not issue_registry.async_get_issue(HA_DOMAIN, issue_id)
|
||||
|
||||
|
||||
async def test_remove_entry_handles_callback_error(
|
||||
|
@ -911,6 +928,49 @@ async def test_reauth_notification(hass: HomeAssistant) -> None:
|
|||
assert "config_entry_reconfigure" not in notifications
|
||||
|
||||
|
||||
async def test_reauth_issue(hass: HomeAssistant) -> None:
|
||||
"""Test that we create/delete an issue when source is reauth."""
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
entry = MockConfigEntry(title="test_title", domain="test")
|
||||
|
||||
mock_setup_entry = AsyncMock(side_effect=ConfigEntryAuthFailed())
|
||||
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
|
||||
mock_platform(hass, "test.config_flow", None)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
await entry.async_setup(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
flows = hass.config_entries.flow.async_progress_by_handler("test")
|
||||
assert len(flows) == 1
|
||||
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue_id = f"config_entry_reauth_test_{entry.entry_id}"
|
||||
issue = issue_registry.async_get_issue(HA_DOMAIN, issue_id)
|
||||
assert issue == ir.IssueEntry(
|
||||
active=True,
|
||||
breaks_in_ha_version=None,
|
||||
created=ANY,
|
||||
data={"flow_id": flows[0]["flow_id"]},
|
||||
dismissed_version=None,
|
||||
domain=HA_DOMAIN,
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain="test",
|
||||
issue_id=issue_id,
|
||||
learn_more_url=None,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="config_entry_reauth",
|
||||
translation_placeholders=None,
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(issue.data["flow_id"], {})
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_discovery_notification_not_created(hass: HomeAssistant) -> None:
|
||||
"""Test that we not create a notification when discovery is aborted."""
|
||||
mock_integration(hass, MockModule("test"))
|
||||
|
|
Loading…
Reference in New Issue