diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 52904cb8d358..dc684a457700 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -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( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2200831e576d..a96716ff84bf 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -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] diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 6a6e48caa7ec..9a3e3a0f5e0e 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -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) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6573a83b0610..a55657d792c0 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -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"}' + ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 672dbb9ae644..247a34c078bc 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -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")