From a02f7a3d399e84c9557d36b45b00e9421d658a40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 May 2024 21:00:34 +0900 Subject: [PATCH 1/3] Add state check to config entry setup to ensure it cannot be setup twice The config entry manager had a check to make sure the state was in the NOT_LOADED state, but since the entry can retry setup we should check in the entry async_setup to verify that the entry is not already loaded or in the progress of setting up before allowing setup to proceed --- homeassistant/config_entries.py | 9 +++++++ tests/test_config_entries.py | 44 +++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index de0fda400b209f..710a07d0352547 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -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" + f" {self.state} state" + ) self._async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None) if self.supports_unload is None: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index adda926458c532..ec28008dded7a8 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -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) @@ -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", [ @@ -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) From d11f47858c3600d208e44b840dc2751a4bb850bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 May 2024 21:29:26 +0900 Subject: [PATCH 2/3] fixes --- tests/test_config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index ec28008dded7a8..f52dd8cceb94d9 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -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 From 7edc6fda29773e3dfa71ddfcaf6f6226e3307d95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 May 2024 21:31:47 +0900 Subject: [PATCH 3/3] fix --- tests/components/upnp/test_config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index a3d2b97f3edcf6..a4598346a51143 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -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) @@ -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) @@ -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) @@ -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)