Return FlowResultType.ABORT when violating single_config_entry (#111637)
* Return FlowResultType.ABORT when violating single_config_entry * Fix translations * Fix tests
This commit is contained in:
parent
4281f648d2
commit
fc4b18b907
4 changed files with 77 additions and 58 deletions
|
@ -1,4 +1,10 @@
|
||||||
{
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||||
|
},
|
||||||
|
"step": {}
|
||||||
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"country_not_configured": {
|
"country_not_configured": {
|
||||||
"title": "The country has not been configured",
|
"title": "The country has not been configured",
|
||||||
|
|
|
@ -1008,6 +1008,8 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
|
||||||
if not context or "source" not in context:
|
if not context or "source" not in context:
|
||||||
raise KeyError("Context not set or doesn't have a source set")
|
raise KeyError("Context not set or doesn't have a source set")
|
||||||
|
|
||||||
|
flow_id = uuid_util.random_uuid_hex()
|
||||||
|
|
||||||
# Avoid starting a config flow on an integration that only supports
|
# Avoid starting a config flow on an integration that only supports
|
||||||
# a single config entry, but which already has an entry
|
# a single config entry, but which already has an entry
|
||||||
if (
|
if (
|
||||||
|
@ -1015,13 +1017,14 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
|
||||||
and await _support_single_config_entry_only(self.hass, handler)
|
and await _support_single_config_entry_only(self.hass, handler)
|
||||||
and self.config_entries.async_entries(handler, include_ignore=False)
|
and self.config_entries.async_entries(handler, include_ignore=False)
|
||||||
):
|
):
|
||||||
raise HomeAssistantError(
|
return FlowResult(
|
||||||
"Cannot start a config flow, the integration"
|
type=data_entry_flow.FlowResultType.ABORT,
|
||||||
" supports only a single config entry"
|
flow_id=flow_id,
|
||||||
" but already has one"
|
handler=handler,
|
||||||
|
reason="single_instance_allowed",
|
||||||
|
translation_domain=HA_DOMAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
flow_id = uuid_util.random_uuid_hex()
|
|
||||||
loop = self.hass.loop
|
loop = self.hass.loop
|
||||||
|
|
||||||
if context["source"] == SOURCE_IMPORT:
|
if context["source"] == SOURCE_IMPORT:
|
||||||
|
@ -1111,6 +1114,21 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
|
||||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# Avoid adding a config entry for a integration
|
||||||
|
# that only supports a single config entry, but already has an entry
|
||||||
|
if (
|
||||||
|
await _support_single_config_entry_only(self.hass, flow.handler)
|
||||||
|
and flow.context["source"] != SOURCE_IGNORE
|
||||||
|
and self.config_entries.async_entries(flow.handler, include_ignore=False)
|
||||||
|
):
|
||||||
|
return FlowResult(
|
||||||
|
type=data_entry_flow.FlowResultType.ABORT,
|
||||||
|
flow_id=flow.flow_id,
|
||||||
|
handler=flow.handler,
|
||||||
|
reason="single_instance_allowed",
|
||||||
|
translation_domain=HA_DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
# Check if config entry exists with unique ID. Unload it.
|
# Check if config entry exists with unique ID. Unload it.
|
||||||
existing_entry = None
|
existing_entry = None
|
||||||
|
|
||||||
|
@ -1398,18 +1416,6 @@ class ConfigEntries:
|
||||||
f"An entry with the id {entry.entry_id} already exists."
|
f"An entry with the id {entry.entry_id} already exists."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Avoid adding a config entry for a integration
|
|
||||||
# that only supports a single config entry, but already has an entry
|
|
||||||
if (
|
|
||||||
await _support_single_config_entry_only(self.hass, entry.domain)
|
|
||||||
and entry.source != SOURCE_IGNORE
|
|
||||||
and self.async_entries(entry.domain, include_ignore=False)
|
|
||||||
):
|
|
||||||
raise HomeAssistantError(
|
|
||||||
f"An entry for {entry.domain} already exists,"
|
|
||||||
f" but integration supports only one config entry"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._entries[entry.entry_id] = entry
|
self._entries[entry.entry_id] = entry
|
||||||
self._async_dispatch(ConfigEntryChange.ADDED, entry)
|
self._async_dispatch(ConfigEntryChange.ADDED, entry)
|
||||||
await self.async_setup(entry.entry_id)
|
await self.async_setup(entry.entry_id)
|
||||||
|
|
|
@ -157,6 +157,7 @@ class FlowResult(TypedDict, total=False):
|
||||||
result: Any
|
result: Any
|
||||||
step_id: str
|
step_id: str
|
||||||
title: str
|
title: str
|
||||||
|
translation_domain: str
|
||||||
type: FlowResultType
|
type: FlowResultType
|
||||||
url: str
|
url: str
|
||||||
version: int
|
version: int
|
||||||
|
|
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from contextlib import AbstractContextManager, nullcontext as does_not_raise
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -4435,41 +4434,32 @@ async def test_hashable_non_string_unique_id(
|
||||||
assert entries.get_entry_by_domain_and_unique_id("test", unique_id) is None
|
assert entries.get_entry_by_domain_and_unique_id("test", unique_id) is None
|
||||||
|
|
||||||
|
|
||||||
RAISES_SINGLE_ENTRY_ERROR = pytest.raises(
|
|
||||||
HomeAssistantError,
|
|
||||||
match=(
|
|
||||||
r"Cannot start a config flow, "
|
|
||||||
r"the integration supports only a single config entry but already has one"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("source", "user_input", "expectation", "expected_result"),
|
("source", "user_input", "expected_result"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_IGNORE,
|
config_entries.SOURCE_IGNORE,
|
||||||
{"unique_id": "blah", "title": "blah"},
|
{"unique_id": "blah", "title": "blah"},
|
||||||
does_not_raise(),
|
|
||||||
{"type": data_entry_flow.FlowResultType.CREATE_ENTRY},
|
{"type": data_entry_flow.FlowResultType.CREATE_ENTRY},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_REAUTH,
|
config_entries.SOURCE_REAUTH,
|
||||||
None,
|
None,
|
||||||
does_not_raise(),
|
|
||||||
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_UNIGNORE,
|
config_entries.SOURCE_UNIGNORE,
|
||||||
None,
|
None,
|
||||||
does_not_raise(),
|
|
||||||
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_USER,
|
config_entries.SOURCE_USER,
|
||||||
None,
|
None,
|
||||||
RAISES_SINGLE_ENTRY_ERROR,
|
{
|
||||||
{},
|
"type": data_entry_flow.FlowResultType.ABORT,
|
||||||
|
"reason": "single_instance_allowed",
|
||||||
|
"translation_domain": HA_DOMAIN,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -4478,7 +4468,6 @@ async def test_starting_config_flow_on_single_config_entry(
|
||||||
manager: config_entries.ConfigEntries,
|
manager: config_entries.ConfigEntries,
|
||||||
source: str,
|
source: str,
|
||||||
user_input: dict,
|
user_input: dict,
|
||||||
expectation: AbstractContextManager,
|
|
||||||
expected_result: dict,
|
expected_result: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test starting a config flow for a single config entry integration.
|
"""Test starting a config flow for a single config entry integration.
|
||||||
|
@ -4520,7 +4509,7 @@ async def test_starting_config_flow_on_single_config_entry(
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.loader.async_get_integration",
|
"homeassistant.loader.async_get_integration",
|
||||||
return_value=integration,
|
return_value=integration,
|
||||||
), expectation:
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
"comp", context={"source": source}, data=user_input
|
"comp", context={"source": source}, data=user_input
|
||||||
)
|
)
|
||||||
|
@ -4530,30 +4519,26 @@ async def test_starting_config_flow_on_single_config_entry(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("source", "user_input", "expectation", "expected_result"),
|
("source", "user_input", "expected_result"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_IGNORE,
|
config_entries.SOURCE_IGNORE,
|
||||||
{"unique_id": "blah", "title": "blah"},
|
{"unique_id": "blah", "title": "blah"},
|
||||||
does_not_raise(),
|
|
||||||
{"type": data_entry_flow.FlowResultType.CREATE_ENTRY},
|
{"type": data_entry_flow.FlowResultType.CREATE_ENTRY},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_REAUTH,
|
config_entries.SOURCE_REAUTH,
|
||||||
None,
|
None,
|
||||||
does_not_raise(),
|
|
||||||
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_UNIGNORE,
|
config_entries.SOURCE_UNIGNORE,
|
||||||
None,
|
None,
|
||||||
does_not_raise(),
|
|
||||||
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
config_entries.SOURCE_USER,
|
config_entries.SOURCE_USER,
|
||||||
None,
|
None,
|
||||||
does_not_raise(),
|
|
||||||
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -4563,7 +4548,6 @@ async def test_starting_config_flow_on_single_config_entry_2(
|
||||||
manager: config_entries.ConfigEntries,
|
manager: config_entries.ConfigEntries,
|
||||||
source: str,
|
source: str,
|
||||||
user_input: dict,
|
user_input: dict,
|
||||||
expectation: AbstractContextManager,
|
|
||||||
expected_result: dict,
|
expected_result: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test starting a config flow for a single config entry integration.
|
"""Test starting a config flow for a single config entry integration.
|
||||||
|
@ -4597,7 +4581,7 @@ async def test_starting_config_flow_on_single_config_entry_2(
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.loader.async_get_integration",
|
"homeassistant.loader.async_get_integration",
|
||||||
return_value=integration,
|
return_value=integration,
|
||||||
), expectation:
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
"comp", context={"source": source}, data=user_input
|
"comp", context={"source": source}, data=user_input
|
||||||
)
|
)
|
||||||
|
@ -4610,6 +4594,19 @@ async def test_avoid_adding_second_config_entry_on_single_config_entry(
|
||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we cannot add a second entry for a single config entry integration."""
|
"""Test that we cannot add a second entry for a single config entry integration."""
|
||||||
|
|
||||||
|
class TestFlow(config_entries.ConfigFlow):
|
||||||
|
"""Test flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Test user step."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(step_id="user")
|
||||||
|
|
||||||
|
return self.async_create_entry(title="yo", data={})
|
||||||
|
|
||||||
integration = loader.Integration(
|
integration = loader.Integration(
|
||||||
hass,
|
hass,
|
||||||
"components.comp",
|
"components.comp",
|
||||||
|
@ -4622,27 +4619,36 @@ async def test_avoid_adding_second_config_entry_on_single_config_entry(
|
||||||
"single_config_entry": True,
|
"single_config_entry": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
entry = MockConfigEntry(
|
mock_integration(hass, MockModule("comp"))
|
||||||
domain="comp",
|
mock_platform(hass, "comp.config_flow", None)
|
||||||
unique_id="1234",
|
|
||||||
title="Test",
|
|
||||||
data={"vendor": "data"},
|
|
||||||
options={"vendor": "options"},
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.loader.async_get_integration",
|
"homeassistant.loader.async_get_integration",
|
||||||
return_value=integration,
|
return_value=integration,
|
||||||
), pytest.raises(
|
), patch.dict(config_entries.HANDLERS, {"comp": TestFlow}):
|
||||||
HomeAssistantError,
|
# Start a flow
|
||||||
match=r"An entry for comp already exists, but integration supports only one config entry",
|
result = await manager.flow.async_init(
|
||||||
):
|
"comp", context={"source": config_entries.SOURCE_USER}
|
||||||
await hass.config_entries.async_add(
|
|
||||||
MockConfigEntry(
|
|
||||||
title="Second comp entry", domain="comp", data={"vendor": "data2"}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
|
||||||
|
# Add a config entry
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain="comp",
|
||||||
|
unique_id="1234",
|
||||||
|
title="Test",
|
||||||
|
data={"vendor": "data"},
|
||||||
|
options={"vendor": "options"},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
# Finish the in progress flow
|
||||||
|
result = await manager.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={"host": "127.0.0.1"}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "single_instance_allowed"
|
||||||
|
assert result["translation_domain"] == HA_DOMAIN
|
||||||
|
|
||||||
|
|
||||||
async def test_in_progress_get_canceled_when_entry_is_created(
|
async def test_in_progress_get_canceled_when_entry_is_created(
|
||||||
|
|
Loading…
Add table
Reference in a new issue