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": {
|
"system_health": {
|
||||||
|
|
|
@ -29,6 +29,7 @@ from .const import DOMAIN
|
||||||
@callback
|
@callback
|
||||||
def async_setup(hass: HomeAssistant) -> None:
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
"""Set up the repairs websocket API."""
|
"""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_ignore_issue)
|
||||||
websocket_api.async_register_command(hass, ws_list_issues)
|
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"]))
|
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
|
@callback
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
|
|
|
@ -23,15 +23,23 @@ from typing import TYPE_CHECKING, Any, Self, TypeVar, cast
|
||||||
from . import data_entry_flow, loader
|
from . import data_entry_flow, loader
|
||||||
from .components import persistent_notification
|
from .components import persistent_notification
|
||||||
from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform
|
from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
from .core import CALLBACK_TYPE, CoreState, Event, HassJob, HomeAssistant, callback
|
from .core import (
|
||||||
from .data_entry_flow import FlowResult
|
CALLBACK_TYPE,
|
||||||
|
DOMAIN as HA_DOMAIN,
|
||||||
|
CoreState,
|
||||||
|
Event,
|
||||||
|
HassJob,
|
||||||
|
HomeAssistant,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
from .data_entry_flow import FLOW_NOT_COMPLETE_STEPS, FlowResult
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
ConfigEntryAuthFailed,
|
ConfigEntryAuthFailed,
|
||||||
ConfigEntryError,
|
ConfigEntryError,
|
||||||
ConfigEntryNotReady,
|
ConfigEntryNotReady,
|
||||||
HomeAssistantError,
|
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.debounce import Debouncer
|
||||||
from .helpers.dispatcher import async_dispatcher_send
|
from .helpers.dispatcher import async_dispatcher_send
|
||||||
from .helpers.event import (
|
from .helpers.event import (
|
||||||
|
@ -793,7 +801,7 @@ class ConfigEntry:
|
||||||
if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})):
|
if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})):
|
||||||
# Reauth flow already in progress for this entry
|
# Reauth flow already in progress for this entry
|
||||||
return
|
return
|
||||||
await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
self.domain,
|
self.domain,
|
||||||
context={
|
context={
|
||||||
"source": SOURCE_REAUTH,
|
"source": SOURCE_REAUTH,
|
||||||
|
@ -804,6 +812,21 @@ class ConfigEntry:
|
||||||
| (context or {}),
|
| (context or {}),
|
||||||
data=self.data | (data 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
|
@callback
|
||||||
def async_get_active_flows(
|
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):
|
if not self._async_has_other_discovery_flows(flow.flow_id):
|
||||||
persistent_notification.async_dismiss(self.hass, DISCOVERY_NOTIFICATION_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:
|
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -1230,12 +1261,15 @@ class ConfigEntries:
|
||||||
ent_reg.async_clear_config_entry(entry_id)
|
ent_reg.async_clear_config_entry(entry_id)
|
||||||
|
|
||||||
# If the configuration entry is removed during reauth, it should
|
# 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(
|
for progress_flow in self.hass.config_entries.flow.async_progress_by_handler(
|
||||||
entry.domain, match_context={"entry_id": entry_id, "source": SOURCE_REAUTH}
|
entry.domain, match_context={"entry_id": entry_id, "source": SOURCE_REAUTH}
|
||||||
):
|
):
|
||||||
if "flow_id" in progress_flow:
|
if "flow_id" in progress_flow:
|
||||||
self.hass.config_entries.flow.async_abort(progress_flow["flow_id"])
|
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
|
# 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
|
# 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",
|
"homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth",
|
||||||
return_value={
|
return_value={
|
||||||
"type": data_entry_flow.FlowResultType.FORM,
|
"type": data_entry_flow.FlowResultType.FORM,
|
||||||
|
"flow_id": "mock_flow",
|
||||||
"step_id": "reauth_confirm",
|
"step_id": "reauth_confirm",
|
||||||
},
|
},
|
||||||
) as mock_async_step_reauth:
|
) 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",
|
"homeassistant.components.aussie_broadband.config_flow.ConfigFlow.async_step_reauth",
|
||||||
return_value={
|
return_value={
|
||||||
"type": data_entry_flow.FlowResultType.FORM,
|
"type": data_entry_flow.FlowResultType.FORM,
|
||||||
|
"flow_id": "mock_flow",
|
||||||
"step_id": "reauth_confirm",
|
"step_id": "reauth_confirm",
|
||||||
},
|
},
|
||||||
) as mock_async_step_reauth:
|
) as mock_async_step_reauth:
|
||||||
|
|
|
@ -579,3 +579,78 @@ async def test_fix_issue_aborted(
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
assert len(msg["result"]["issues"]) == 1
|
assert len(msg["result"]["issues"]) == 1
|
||||||
assert msg["result"]["issues"][0] == first_issue
|
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",
|
"homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth",
|
||||||
return_value={
|
return_value={
|
||||||
"type": data_entry_flow.FlowResultType.FORM,
|
"type": data_entry_flow.FlowResultType.FORM,
|
||||||
|
"flow_id": "mock_flow",
|
||||||
"step_id": "reauth_confirm",
|
"step_id": "reauth_confirm",
|
||||||
},
|
},
|
||||||
) as mock_async_step_reauth:
|
) as mock_async_step_reauth:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from collections.abc import Generator
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
@ -19,7 +19,13 @@ from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
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.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
ConfigEntryAuthFailed,
|
ConfigEntryAuthFailed,
|
||||||
|
@ -27,7 +33,7 @@ from homeassistant.exceptions import (
|
||||||
ConfigEntryNotReady,
|
ConfigEntryNotReady,
|
||||||
HomeAssistantError,
|
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.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
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):
|
async def async_step_reauth(self, data):
|
||||||
"""Mock Reauth."""
|
"""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(
|
with patch.dict(
|
||||||
config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler}
|
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 flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH
|
||||||
assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR
|
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)
|
await manager.async_remove(entry.entry_id)
|
||||||
|
|
||||||
flows = hass.config_entries.flow.async_progress_by_handler("test")
|
flows = hass.config_entries.flow.async_progress_by_handler("test")
|
||||||
assert len(flows) == 0
|
assert len(flows) == 0
|
||||||
|
assert not issue_registry.async_get_issue(HA_DOMAIN, issue_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_remove_entry_handles_callback_error(
|
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
|
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:
|
async def test_discovery_notification_not_created(hass: HomeAssistant) -> None:
|
||||||
"""Test that we not create a notification when discovery is aborted."""
|
"""Test that we not create a notification when discovery is aborted."""
|
||||||
mock_integration(hass, MockModule("test"))
|
mock_integration(hass, MockModule("test"))
|
||||||
|
|
Loading…
Reference in New Issue