Handle late abort when creating subentry (#145765)

* Handle late abort when creating subentry

* Move error handling to the base class

* Narrow down expected error in test
pull/144056/head
Erik Montnemery 2025-05-28 12:26:28 +02:00 committed by GitHub
parent e4cc842584
commit a857461059
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 5 deletions

View File

@ -543,8 +543,17 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]):
flow.cur_step = result
return result
# We pass a copy of the result because we're mutating our version
result = await self.async_finish_flow(flow, result.copy())
try:
# We pass a copy of the result because we're mutating our version
result = await self.async_finish_flow(flow, result.copy())
except AbortFlow as err:
result = self._flow_result(
type=FlowResultType.ABORT,
flow_id=flow.flow_id,
handler=flow.handler,
reason=err.reason,
description_placeholders=err.description_placeholders,
)
# _async_finish_flow may change result type, check it again
if result["type"] == FlowResultType.FORM:

View File

@ -1526,6 +1526,88 @@ async def test_subentry_reconfigure_flow(hass: HomeAssistant, client) -> None:
}
async def test_subentry_flow_abort_duplicate(hass: HomeAssistant, client) -> None:
"""Test we can handle a subentry flow raising due to unique_id collision."""
class TestFlow(core_ce.ConfigFlow):
class SubentryFlowHandler(core_ce.ConfigSubentryFlow):
async def async_step_user(self, user_input=None):
return await self.async_step_finish()
async def async_step_finish(self, user_input=None):
if user_input:
return self.async_create_entry(
title="Mock title", data=user_input, unique_id="test"
)
return self.async_show_form(
step_id="finish", data_schema=vol.Schema({"enabled": bool})
)
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: core_ce.ConfigEntry
) -> dict[str, type[core_ce.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.config_flow", None)
MockConfigEntry(
domain="test",
entry_id="test1",
source="bla",
subentries_data=[
core_ce.ConfigSubentryData(
data={},
subentry_id="mock_id",
subentry_type="test",
title="Title",
unique_id="test",
)
],
).add_to_hass(hass)
entry = hass.config_entries.async_entries()[0]
with mock_config_flow("test", TestFlow):
url = "/api/config/config_entries/subentries/flow"
resp = await client.post(url, json={"handler": [entry.entry_id, "test"]})
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data.pop("flow_id")
assert data == {
"type": "form",
"handler": ["test1", "test"],
"step_id": "finish",
"data_schema": [{"name": "enabled", "type": "boolean"}],
"description_placeholders": None,
"errors": None,
"last_step": None,
"preview": None,
}
with mock_config_flow("test", TestFlow):
resp = await client.post(
f"/api/config/config_entries/subentries/flow/{flow_id}",
json={"enabled": True},
)
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": ["test1", "test"],
"reason": "already_configured",
"type": "abort",
"description_placeholders": None,
}
async def test_subentry_does_not_support_reconfigure(
hass: HomeAssistant, client: TestClient
) -> None:

View File

@ -2226,7 +2226,7 @@ async def test_entry_subentry_no_context(
@pytest.mark.parametrize(
("unique_id", "expected_result"),
[(None, does_not_raise()), ("test", pytest.raises(HomeAssistantError))],
[(None, does_not_raise()), ("test", pytest.raises(data_entry_flow.AbortFlow))],
)
async def test_entry_subentry_duplicate(
hass: HomeAssistant,

View File

@ -886,8 +886,8 @@ async def test_show_progress_fires_only_when_changed(
) # change (description placeholder)
async def test_abort_flow_exception(manager: MockFlowManager) -> None:
"""Test that the AbortFlow exception works."""
async def test_abort_flow_exception_step(manager: MockFlowManager) -> None:
"""Test that the AbortFlow exception works in a step."""
@manager.mock_reg_handler("test")
class TestFlow(data_entry_flow.FlowHandler):
@ -900,6 +900,33 @@ async def test_abort_flow_exception(manager: MockFlowManager) -> None:
assert form["description_placeholders"] == {"placeholder": "yo"}
async def test_abort_flow_exception_finish_flow(hass: HomeAssistant) -> None:
"""Test that the AbortFlow exception works when finishing a flow."""
class TestFlow(data_entry_flow.FlowHandler):
VERSION = 1
async def async_step_init(self, input):
"""Return init form with one input field 'count'."""
return self.async_create_entry(title="init", data=input)
class FlowManager(data_entry_flow.FlowManager):
async def async_create_flow(self, handler_key, *, context, data):
"""Create a test flow."""
return TestFlow()
async def async_finish_flow(self, flow, result):
"""Raise AbortFlow."""
raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"})
manager = FlowManager(hass)
form = await manager.async_init("test")
assert form["type"] == data_entry_flow.FlowResultType.ABORT
assert form["reason"] == "mock-reason"
assert form["description_placeholders"] == {"placeholder": "yo"}
async def test_init_unknown_flow(manager: MockFlowManager) -> None:
"""Test that UnknownFlow is raised when async_create_flow returns None."""