Add reconfigure step to config flow (#108794)

* Initial commit reconfigure

* test config config_entries

* Fix reconfigure

* test_config_entries

* review comment

* No reconfigure if reauth ongoing

* Fix tests

* Fix tests

* handle source creating flows

* combine

* No black

* Also check reconfigure in reauth flow

* Fix support

* Add entry id

* reset data entry flow

* Mods

* context data

* reset formatting

* Fix config flow platforms

* Fix tests

* Fix step message

* Handling reconfigure step

* Fix more tests

* Config entries tests

* entry_id always means reconfigure

* Mods

* Remove no longer valid exception

* Fixes

* reset silabs test

* dev reg

* resets

* assist pipeline

* Adjust config_entries

* Fix

* Fixes

* docstrings

* Review comment

* docstring
This commit is contained in:
G Johansson 2024-03-01 12:29:35 +01:00 committed by GitHub
parent fd9e9ebf50
commit 9989a63cdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 358 additions and 13 deletions

View File

@ -13,6 +13,7 @@ from homeassistant import config_entries, data_entry_flow
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
from homeassistant.components import websocket_api
from homeassistant.components.http import HomeAssistantView, require_admin
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import DependencyError, Unauthorized
import homeassistant.helpers.config_validation as cv
@ -153,10 +154,26 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def post(self, request: web.Request) -> web.Response:
"""Handle a POST request."""
@RequestDataValidator(
vol.Schema(
{
vol.Required("handler"): vol.Any(str, list),
vol.Optional("show_advanced_options", default=False): cv.boolean,
vol.Optional("entry_id"): cv.string,
},
extra=vol.ALLOW_EXTRA,
)
)
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Initialize a POST request for a config entry flow."""
return await self._post_impl(request, data)
async def _post_impl(
self, request: web.Request, data: dict[str, Any]
) -> web.Response:
"""Handle a POST request for a config entry flow."""
try:
return await super().post(request)
return await super()._post_impl(request, data)
except DependencyError as exc:
return web.Response(
text=f"Failed dependencies {', '.join(exc.failed_dependencies)}",
@ -167,6 +184,9 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""Return context."""
context = super().get_context(data)
context["source"] = config_entries.SOURCE_USER
if entry_id := data.get("entry_id"):
context["source"] = config_entries.SOURCE_RECONFIGURE
context["entry_id"] = entry_id
return context
def _prepare_result_json(

View File

@ -104,6 +104,9 @@ SOURCE_UNIGNORE = "unignore"
# This is used to signal that re-authentication is required by the user.
SOURCE_REAUTH = "reauth"
# This is used to initiate a reconfigure flow by the user.
SOURCE_RECONFIGURE = "reconfigure"
HANDLERS: Registry[str, type[ConfigFlow]] = Registry()
STORAGE_KEY = "core.config_entries"
@ -343,6 +346,9 @@ class ConfigEntry:
# Supports options
self._supports_options: bool | None = None
# Supports reconfigure
self._supports_reconfigure: bool | None = None
# Listeners to call on update
self.update_listeners: list[UpdateListenerType] = []
@ -361,6 +367,8 @@ class ConfigEntry:
self.reload_lock = asyncio.Lock()
# Reauth lock to prevent concurrent reauth flows
self._reauth_lock = asyncio.Lock()
# Reconfigure lock to prevent concurrent reconfigure flows
self._reconfigure_lock = asyncio.Lock()
self._tasks: set[asyncio.Future[Any]] = set()
self._background_tasks: set[asyncio.Future[Any]] = set()
@ -413,6 +421,20 @@ class ConfigEntry:
)
return self._supports_options or False
@property
def supports_reconfigure(self) -> bool:
"""Return if entry supports config options."""
if self._supports_reconfigure is None and (
handler := HANDLERS.get(self.domain)
):
# work out if handler has support for reconfigure step
object.__setattr__(
self,
"_supports_reconfigure",
hasattr(handler, "async_step_reconfigure"),
)
return self._supports_reconfigure or False
def clear_cache(self) -> None:
"""Clear cached properties."""
with contextlib.suppress(AttributeError):
@ -430,6 +452,7 @@ class ConfigEntry:
"supports_options": self.supports_options,
"supports_remove_device": self.supports_remove_device or False,
"supports_unload": self.supports_unload or False,
"supports_reconfigure": self.supports_reconfigure or False,
"pref_disable_new_entities": self.pref_disable_new_entities,
"pref_disable_polling": self.pref_disable_polling,
"disabled_by": self.disabled_by,
@ -462,7 +485,6 @@ class ConfigEntry:
self.supports_remove_device = await support_remove_from_device(
hass, self.domain
)
try:
component = integration.get_component()
except ImportError as err:
@ -856,8 +878,8 @@ class ConfigEntry:
"""Start a reauth flow."""
# We will check this again in the task when we hold the lock,
# but we also check it now to try to avoid creating the task.
if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})):
# Reauth flow already in progress for this entry
if any(self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})):
# Reauth or Reconfigure flow already in progress for this entry
return
hass.async_create_task(
self._async_init_reauth(hass, context, data),
@ -872,8 +894,10 @@ class ConfigEntry:
) -> None:
"""Start a reauth flow."""
async with self._reauth_lock:
if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})):
# Reauth flow already in progress for this entry
if any(
self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})
):
# Reauth or Reconfigure flow already in progress for this entry
return
result = await hass.config_entries.flow.async_init(
self.domain,
@ -903,6 +927,49 @@ class ConfigEntry:
translation_placeholders={"name": self.title},
)
@callback
def async_start_reconfigure(
self,
hass: HomeAssistant,
context: dict[str, Any] | None = None,
data: dict[str, Any] | None = None,
) -> None:
"""Start a reconfigure flow."""
# We will check this again in the task when we hold the lock,
# but we also check it now to try to avoid creating the task.
if any(self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})):
# Reconfigure or reauth flow already in progress for this entry
return
hass.async_create_task(
self._async_init_reconfigure(hass, context, data),
f"config entry reconfigure {self.title} {self.domain} {self.entry_id}",
)
async def _async_init_reconfigure(
self,
hass: HomeAssistant,
context: dict[str, Any] | None = None,
data: dict[str, Any] | None = None,
) -> None:
"""Start a reconfigure flow."""
async with self._reconfigure_lock:
if any(
self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})
):
# Reconfigure or reauth flow already in progress for this entry
return
await hass.config_entries.flow.async_init(
self.domain,
context={
"source": SOURCE_RECONFIGURE,
"entry_id": self.entry_id,
"title_placeholders": {"name": self.title},
"unique_id": self.unique_id,
}
| (context or {}),
data=self.data | (data or {}),
)
@callback
def async_get_active_flows(
self, hass: HomeAssistant, sources: set[str]

View File

@ -61,6 +61,16 @@ class FlowManagerIndexView(_BaseFlowManagerView):
)
)
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Initialize a POST request.
Override `_post_impl` in subclasses which need
to implement their own `RequestDataValidator`
"""
return await self._post_impl(request, data)
async def _post_impl(
self, request: web.Request, data: dict[str, Any]
) -> web.Response:
"""Handle a POST request."""
if isinstance(data["handler"], list):
handler = tuple(data["handler"])
@ -74,10 +84,8 @@ class FlowManagerIndexView(_BaseFlowManagerView):
)
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
)
except data_entry_flow.UnknownStep as err:
return self.json_message(str(err), HTTPStatus.BAD_REQUEST)
result = self._prepare_result_json(result)

View File

@ -3,6 +3,7 @@ from collections import OrderedDict
from http import HTTPStatus
from unittest.mock import ANY, AsyncMock, patch
from aiohttp.test_utils import TestClient
import pytest
import voluptuous as vol
@ -39,7 +40,7 @@ def mock_test_component(hass):
@pytest.fixture
async def client(hass, hass_client):
async def client(hass, hass_client) -> TestClient:
"""Fixture that can interact with the config manager API."""
await async_setup_component(hass, "http", {})
config_entries.async_setup(hass)
@ -121,6 +122,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None:
"title": "Test 1",
"source": "bla",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supports_reconfigure": False,
"supports_options": True,
"supports_remove_device": False,
"supports_unload": True,
@ -134,6 +136,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None:
"title": "Test 2",
"source": "bla2",
"state": core_ce.ConfigEntryState.SETUP_ERROR.value,
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -147,6 +150,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None:
"title": "Test 3",
"source": "bla3",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -160,6 +164,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None:
"title": "Test 4",
"source": "bla4",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -173,6 +178,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None:
"title": "Test 5",
"source": "bla5",
"state": core_ce.ConfigEntryState.NOT_LOADED.value,
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -521,6 +527,7 @@ async def test_create_account(
"entry_id": entries[0].entry_id,
"source": core_ce.SOURCE_USER,
"state": core_ce.ConfigEntryState.LOADED.value,
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -599,6 +606,7 @@ async def test_two_step_flow(
"entry_id": entries[0].entry_id,
"source": core_ce.SOURCE_USER,
"state": core_ce.ConfigEntryState.LOADED.value,
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1051,6 +1059,7 @@ async def test_get_single(
"reason": None,
"source": "user",
"state": "loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1385,6 +1394,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1399,6 +1409,7 @@ async def test_get_matching_entries_ws(
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1413,6 +1424,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1427,6 +1439,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla4",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1441,6 +1454,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla5",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1466,6 +1480,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1490,6 +1505,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla4",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1504,6 +1520,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla5",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1528,6 +1545,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1542,6 +1560,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1572,6 +1591,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1586,6 +1606,7 @@ async def test_get_matching_entries_ws(
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1600,6 +1621,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1614,6 +1636,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla4",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1628,6 +1651,7 @@ async def test_get_matching_entries_ws(
"reason": None,
"source": "bla5",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1726,6 +1750,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1743,6 +1768,7 @@ async def test_subscribe_entries_ws(
"reason": "Unsupported API",
"source": "bla2",
"state": "setup_error",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1760,6 +1786,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1781,6 +1808,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1803,6 +1831,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1825,6 +1854,7 @@ async def test_subscribe_entries_ws(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1906,6 +1936,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1923,6 +1954,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1946,6 +1978,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1967,6 +2000,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla3",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -1990,6 +2024,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -2012,6 +2047,7 @@ async def test_subscribe_entries_ws_filtered(
"reason": None,
"source": "bla",
"state": "not_loaded",
"supports_reconfigure": False,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
@ -2106,3 +2142,123 @@ async def test_flow_with_multiple_schema_errors_base(
"latitude": "required key not provided",
}
}
async def test_supports_reconfigure(
hass: HomeAssistant, client, enable_custom_integrations: None
) -> None:
"""Test a flow that support reconfigure step."""
mock_platform(hass, "test.config_flow", None)
mock_integration(
hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True))
)
class TestFlow(core_ce.ConfigFlow):
VERSION = 1
async def async_step_user(self, user_input=None):
return self.async_create_entry(
title="Test Entry", data={"secret": "account_token"}
)
async def async_step_reconfigure(self, user_input=None):
if user_input is None:
return self.async_show_form(
step_id="reconfigure", data_schema=vol.Schema({})
)
return self.async_create_entry(
title="Test Entry", data={"secret": "account_token"}
)
with patch.dict(HANDLERS, {"test": TestFlow}):
resp = await client.post(
"/api/config/config_entries/flow",
json={"handler": "test", "entry_id": "1"},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data.pop("flow_id")
assert data == {
"type": "form",
"handler": "test",
"step_id": "reconfigure",
"data_schema": [],
"last_step": None,
"preview": None,
"description_placeholders": None,
"errors": None,
}
with patch.dict(HANDLERS, {"test": TestFlow}):
resp = await client.post(
f"/api/config/config_entries/flow/{flow_id}",
json={},
)
assert resp.status == HTTPStatus.OK
entries = hass.config_entries.async_entries("test")
assert len(entries) == 1
data = await resp.json()
data.pop("flow_id")
assert data == {
"handler": "test",
"title": "Test Entry",
"type": "create_entry",
"version": 1,
"result": {
"disabled_by": None,
"domain": "test",
"entry_id": entries[0].entry_id,
"source": core_ce.SOURCE_RECONFIGURE,
"state": core_ce.ConfigEntryState.LOADED.value,
"supports_reconfigure": True,
"supports_options": False,
"supports_remove_device": False,
"supports_unload": False,
"pref_disable_new_entities": False,
"pref_disable_polling": False,
"title": "Test Entry",
"reason": None,
},
"description": None,
"description_placeholders": None,
"options": {},
"minor_version": 1,
}
async def test_does_not_support_reconfigure(
hass: HomeAssistant, client: TestClient, enable_custom_integrations: None
) -> None:
"""Test a flow that does not support reconfigure step."""
mock_platform(hass, "test.config_flow", None)
mock_integration(
hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True))
)
class TestFlow(core_ce.ConfigFlow):
VERSION = 1
async def async_step_user(self, user_input=None):
return self.async_create_entry(
title="Test Entry", data={"secret": "account_token"}
)
with patch.dict(HANDLERS, {"test": TestFlow}):
resp = await client.post(
"/api/config/config_entries/flow",
json={"handler": "test", "entry_id": "1"},
)
assert resp.status == HTTPStatus.BAD_REQUEST
response = await resp.text()
assert (
response
== '{"message":"Handler ConfigEntriesFlowManager doesn\'t support step reconfigure"}'
)

View File

@ -70,6 +70,10 @@ def mock_handlers() -> Generator[None, None, None]:
return self.async_show_form(step_id="reauth_confirm")
return self.async_abort(reason="test")
async def async_step_reconfigure(self, data):
"""Mock Reauth."""
return await self.async_step_reauth_confirm()
with patch.dict(
config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler}
):
@ -826,6 +830,8 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None:
"_tries",
"_setup_again_job",
"_supports_options",
"_reconfigure_lock",
"supports_reconfigure",
}
entry = MockConfigEntry(entry_id="mock-entry")
@ -4062,6 +4068,94 @@ async def test_reauth(hass: HomeAssistant) -> None:
assert len(hass.config_entries.flow.async_progress()) == 1
async def test_reconfigure(hass: HomeAssistant) -> None:
"""Test the async_reconfigure_helper."""
entry = MockConfigEntry(title="test_title", domain="test")
entry2 = MockConfigEntry(title="test_title", domain="test")
mock_setup_entry = AsyncMock(return_value=True)
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
mock_platform(hass, "test.config_flow", None)
await entry.async_setup(hass)
await hass.async_block_till_done()
flow = hass.config_entries.flow
with patch.object(flow, "async_init", wraps=flow.async_init) as mock_init:
entry.async_start_reconfigure(
hass,
context={"extra_context": "some_extra_context"},
data={"extra_data": 1234},
)
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["context"]["entry_id"] == entry.entry_id
assert flows[0]["context"]["source"] == config_entries.SOURCE_RECONFIGURE
assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"}
assert flows[0]["context"]["extra_context"] == "some_extra_context"
assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234
assert entry.entry_id != entry2.entry_id
# Check that we can't start duplicate reconfigure flows
entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
# Check that we can't start duplicate reconfigure flows when the context is different
entry.async_start_reconfigure(hass, {"diff": "diff"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
# Check that we can start a reconfigure flow for a different entry
entry2.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 2
# Abort all existing flows
for flow in hass.config_entries.flow.async_progress():
hass.config_entries.flow.async_abort(flow["flow_id"])
await hass.async_block_till_done()
# Check that we can't start duplicate reconfigure flows
# without blocking between flows
entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
# Abort all existing flows
for flow in hass.config_entries.flow.async_progress():
hass.config_entries.flow.async_abort(flow["flow_id"])
await hass.async_block_till_done()
# Check that we can't start reconfigure flows with active reauth flow
entry.async_start_reauth(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
# Abort all existing flows
for flow in hass.config_entries.flow.async_progress():
hass.config_entries.flow.async_abort(flow["flow_id"])
await hass.async_block_till_done()
# Check that we can't start reauth flows with active reconfigure flow
entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
entry.async_start_reauth(hass, {"extra_context": "some_extra_context"})
await hass.async_block_till_done()
assert len(hass.config_entries.flow.async_progress()) == 1
async def test_get_active_flows(hass: HomeAssistant) -> None:
"""Test the async_get_active_flows helper."""
entry = MockConfigEntry(title="test_title", domain="test")