Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add state check to config entry setup to ensure it cannot be setup twice #117193

Merged
merged 3 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions homeassistant/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,15 @@ async def async_setup(

# Only store setup result as state if it was not forwarded.
if domain_is_integration := self.domain == integration.domain:
if self.state in (
ConfigEntryState.LOADED,
ConfigEntryState.SETUP_IN_PROGRESS,
):
raise OperationNotAllowed(
f"The config entry {self.title} ({self.domain}) with entry_id"
f" {self.entry_id} cannot be setup because is already loaded in the"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
f" {self.entry_id} cannot be setup because is already loaded in the"
f" {self.entry_id} cannot be set up because is already loaded in the"

Taking another look, maybe also:

Suggested change
f" {self.entry_id} cannot be setup because is already loaded in the"
f" {self.entry_id} cannot be set up because it is already loaded in the"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do a followup as I copied the message from the other location

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding a lock check to async_setup and async_unload to ensure we never run them without the lock in #117214. I fixed the text there

f" {self.state} state"
)
self._async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None)

if self.supports_unload is None:
Expand Down
8 changes: 4 additions & 4 deletions tests/components/upnp/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant) ->
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
},
source=config_entries.SOURCE_SSDP,
state=config_entries.ConfigEntryState.LOADED,
state=config_entries.ConfigEntryState.NOT_LOADED,
)
entry.add_to_hass(hass)

Expand Down Expand Up @@ -228,7 +228,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant) -
CONFIG_ENTRY_HOST: TEST_HOST,
},
source=config_entries.SOURCE_SSDP,
state=config_entries.ConfigEntryState.LOADED,
state=config_entries.ConfigEntryState.NOT_LOADED,
)
entry.add_to_hass(hass)

Expand Down Expand Up @@ -266,7 +266,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
},
source=config_entries.SOURCE_SSDP,
state=config_entries.ConfigEntryState.LOADED,
state=config_entries.ConfigEntryState.NOT_LOADED,
)
entry.add_to_hass(hass)

Expand Down Expand Up @@ -320,7 +320,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant) -> None
CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS,
},
source=config_entries.SOURCE_SSDP,
state=config_entries.ConfigEntryState.LOADED,
state=config_entries.ConfigEntryState.NOT_LOADED,
)
entry.add_to_hass(hass)

Expand Down
46 changes: 43 additions & 3 deletions tests/test_config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ async def mock_setup_entry_platform(
]

# Setup entry
await entry.async_setup(hass)
await manager.async_setup(entry.entry_id)
await hass.async_block_till_done()

# Check entity state got added
Expand Down Expand Up @@ -1613,7 +1613,9 @@ async def test_entry_reload_succeed(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test that we can reload an entry."""
entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED)
entry = MockConfigEntry(
domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED
)
entry.add_to_hass(hass)

async_setup = AsyncMock(return_value=True)
Expand All @@ -1637,6 +1639,42 @@ async def test_entry_reload_succeed(
assert entry.state is config_entries.ConfigEntryState.LOADED


@pytest.mark.parametrize(
"state",
[
config_entries.ConfigEntryState.LOADED,
config_entries.ConfigEntryState.SETUP_IN_PROGRESS,
],
)
async def test_entry_cannot_be_loaded_twice(
hass: HomeAssistant, state: config_entries.ConfigEntryState
) -> None:
"""Test that a config entry cannot be loaded twice."""
entry = MockConfigEntry(domain="comp", state=state)
entry.add_to_hass(hass)

async_setup = AsyncMock(return_value=True)
async_setup_entry = AsyncMock(return_value=True)
async_unload_entry = AsyncMock(return_value=True)

mock_integration(
hass,
MockModule(
"comp",
async_setup=async_setup,
async_setup_entry=async_setup_entry,
async_unload_entry=async_unload_entry,
),
)
mock_platform(hass, "comp.config_flow", None)

with pytest.raises(config_entries.OperationNotAllowed, match=str(state)):
await entry.async_setup(hass)
assert len(async_setup.mock_calls) == 0
assert len(async_setup_entry.mock_calls) == 0
assert entry.state is state


@pytest.mark.parametrize(
"state",
[
Expand Down Expand Up @@ -4005,7 +4043,9 @@ async def test_entry_reload_concurrency_not_setup_setup(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test multiple reload calls do not cause a reload race."""
entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED)
entry = MockConfigEntry(
domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED
)
entry.add_to_hass(hass)

async_setup = AsyncMock(return_value=True)
Expand Down