Always allow ignore and unignore flows for single config entry integrations (#111631)
* Always allow ignore and unignore flows for single config entry integrations * Update tests/test_config_entries.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
070b411820
commit
6ccf7dea32
2 changed files with 150 additions and 57 deletions
|
@ -1011,9 +1011,9 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
|
||||||
# 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 (
|
||||||
context.get("source") != SOURCE_REAUTH
|
context.get("source") not in {SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_UNIGNORE}
|
||||||
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_has_entry(handler)
|
and self.config_entries.async_entries(handler, include_ignore=False)
|
||||||
):
|
):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Cannot start a config flow, the integration"
|
"Cannot start a config flow, the integration"
|
||||||
|
@ -1391,11 +1391,6 @@ class ConfigEntries:
|
||||||
"""Return entry for a domain with a matching unique id."""
|
"""Return entry for a domain with a matching unique id."""
|
||||||
return self._entries.get_entry_by_domain_and_unique_id(domain, unique_id)
|
return self._entries.get_entry_by_domain_and_unique_id(domain, unique_id)
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_has_entry(self, domain: str) -> bool:
|
|
||||||
"""Return if there are entries for a domain."""
|
|
||||||
return bool(self.async_entries(domain))
|
|
||||||
|
|
||||||
async def async_add(self, entry: ConfigEntry) -> None:
|
async def async_add(self, entry: ConfigEntry) -> None:
|
||||||
"""Add and setup an entry."""
|
"""Add and setup an entry."""
|
||||||
if entry.entry_id in self._entries.data:
|
if entry.entry_id in self._entries.data:
|
||||||
|
@ -1405,9 +1400,11 @@ class ConfigEntries:
|
||||||
|
|
||||||
# Avoid adding a config entry for a integration
|
# Avoid adding a config entry for a integration
|
||||||
# that only supports a single config entry, but already has an entry
|
# that only supports a single config entry, but already has an entry
|
||||||
if await _support_single_config_entry_only(
|
if (
|
||||||
self.hass, entry.domain
|
await _support_single_config_entry_only(self.hass, entry.domain)
|
||||||
) and self.async_has_entry(entry.domain):
|
and entry.source != SOURCE_IGNORE
|
||||||
|
and self.async_entries(entry.domain, include_ignore=False)
|
||||||
|
):
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"An entry for {entry.domain} already exists,"
|
f"An entry for {entry.domain} already exists,"
|
||||||
f" but integration supports only one config entry"
|
f" but integration supports only one config entry"
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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
|
||||||
|
@ -4434,10 +4435,56 @@ 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
|
||||||
|
|
||||||
|
|
||||||
async def test_avoid_starting_config_flow_on_single_config_entry(
|
RAISES_SINGLE_ENTRY_ERROR = pytest.raises(
|
||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
HomeAssistantError,
|
||||||
|
match=(
|
||||||
|
r"Cannot start a config flow, "
|
||||||
|
r"the integration supports only a single config entry but already has one"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("source", "user_input", "expectation", "expected_result"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
{"unique_id": "blah", "title": "blah"},
|
||||||
|
does_not_raise(),
|
||||||
|
{"type": data_entry_flow.FlowResultType.CREATE_ENTRY},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_REAUTH,
|
||||||
|
None,
|
||||||
|
does_not_raise(),
|
||||||
|
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_UNIGNORE,
|
||||||
|
None,
|
||||||
|
does_not_raise(),
|
||||||
|
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_USER,
|
||||||
|
None,
|
||||||
|
RAISES_SINGLE_ENTRY_ERROR,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_starting_config_flow_on_single_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
manager: config_entries.ConfigEntries,
|
||||||
|
source: str,
|
||||||
|
user_input: dict,
|
||||||
|
expectation: AbstractContextManager,
|
||||||
|
expected_result: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we cannot start a config flow for a single config entry integration."""
|
"""Test starting a config flow for a single config entry integration.
|
||||||
|
|
||||||
|
In this test, the integration has one ignored flow and one entry added by user.
|
||||||
|
"""
|
||||||
integration = loader.Integration(
|
integration = loader.Integration(
|
||||||
hass,
|
hass,
|
||||||
"components.comp",
|
"components.comp",
|
||||||
|
@ -4458,17 +4505,105 @@ async def test_avoid_starting_config_flow_on_single_config_entry(
|
||||||
options={"vendor": "options"},
|
options={"vendor": "options"},
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
ignored_entry = MockConfigEntry(
|
||||||
|
domain="comp",
|
||||||
|
unique_id="2345",
|
||||||
|
title="Test",
|
||||||
|
data={"vendor": "data"},
|
||||||
|
options={"vendor": "options"},
|
||||||
|
source=config_entries.SOURCE_IGNORE,
|
||||||
|
)
|
||||||
|
ignored_entry.add_to_hass(hass)
|
||||||
|
|
||||||
mock_platform(hass, "comp.config_flow", None)
|
mock_platform(hass, "comp.config_flow", None)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.loader.async_get_integration",
|
"homeassistant.loader.async_get_integration",
|
||||||
return_value=integration,
|
return_value=integration,
|
||||||
), pytest.raises(
|
), expectation:
|
||||||
HomeAssistantError,
|
result = await hass.config_entries.flow.async_init(
|
||||||
match=r"Cannot start a config flow, the integration supports only a single config entry but already has one",
|
"comp", context={"source": source}, data=user_input
|
||||||
):
|
)
|
||||||
await hass.config_entries.flow.async_init("comp", context={"source": "user"})
|
|
||||||
|
for key in expected_result:
|
||||||
|
assert result[key] == expected_result[key]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("source", "user_input", "expectation", "expected_result"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_IGNORE,
|
||||||
|
{"unique_id": "blah", "title": "blah"},
|
||||||
|
does_not_raise(),
|
||||||
|
{"type": data_entry_flow.FlowResultType.CREATE_ENTRY},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_REAUTH,
|
||||||
|
None,
|
||||||
|
does_not_raise(),
|
||||||
|
{"type": data_entry_flow.FlowResultType.FORM, "step_id": "reauth_confirm"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_UNIGNORE,
|
||||||
|
None,
|
||||||
|
does_not_raise(),
|
||||||
|
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_USER,
|
||||||
|
None,
|
||||||
|
does_not_raise(),
|
||||||
|
{"type": data_entry_flow.FlowResultType.ABORT, "reason": "not_implemented"},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_starting_config_flow_on_single_config_entry_2(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
manager: config_entries.ConfigEntries,
|
||||||
|
source: str,
|
||||||
|
user_input: dict,
|
||||||
|
expectation: AbstractContextManager,
|
||||||
|
expected_result: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Test starting a config flow for a single config entry integration.
|
||||||
|
|
||||||
|
In this test, the integration has one ignored flow but no entry added by user.
|
||||||
|
"""
|
||||||
|
integration = loader.Integration(
|
||||||
|
hass,
|
||||||
|
"components.comp",
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"name": "Comp",
|
||||||
|
"dependencies": [],
|
||||||
|
"requirements": [],
|
||||||
|
"domain": "comp",
|
||||||
|
"single_config_entry": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ignored_entry = MockConfigEntry(
|
||||||
|
domain="comp",
|
||||||
|
unique_id="2345",
|
||||||
|
title="Test",
|
||||||
|
data={"vendor": "data"},
|
||||||
|
options={"vendor": "options"},
|
||||||
|
source=config_entries.SOURCE_IGNORE,
|
||||||
|
)
|
||||||
|
ignored_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
mock_platform(hass, "comp.config_flow", None)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.loader.async_get_integration",
|
||||||
|
return_value=integration,
|
||||||
|
), expectation:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
"comp", context={"source": source}, data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in expected_result:
|
||||||
|
assert result[key] == expected_result[key]
|
||||||
|
|
||||||
|
|
||||||
async def test_avoid_adding_second_config_entry_on_single_config_entry(
|
async def test_avoid_adding_second_config_entry_on_single_config_entry(
|
||||||
|
@ -4566,45 +4701,6 @@ async def test_in_progress_get_canceled_when_entry_is_created(
|
||||||
assert len(manager.async_entries()) == 1
|
assert len(manager.async_entries()) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_start_reauth_still_possible_for_single_config_entry(
|
|
||||||
hass: HomeAssistant, manager: config_entries.ConfigEntries
|
|
||||||
):
|
|
||||||
"""Test that we can still start a reauth flow on a single config entry integration."""
|
|
||||||
integration = loader.Integration(
|
|
||||||
hass,
|
|
||||||
"components.comp",
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"name": "Comp",
|
|
||||||
"dependencies": [],
|
|
||||||
"requirements": [],
|
|
||||||
"domain": "comp",
|
|
||||||
"single_config_entry": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain="comp",
|
|
||||||
unique_id="1234",
|
|
||||||
title="Test",
|
|
||||||
data={"vendor": "data"},
|
|
||||||
options={"vendor": "options"},
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
mock_platform(hass, "comp.config_flow", None)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.loader.async_get_integration",
|
|
||||||
return_value=integration,
|
|
||||||
):
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
"comp", context={"source": config_entries.SOURCE_REAUTH}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
|
||||||
assert result["step_id"] == "reauth_confirm"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_directly_mutating_blocked(
|
async def test_directly_mutating_blocked(
|
||||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue