diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 3eedaff15d0..0dd13677ed5 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -81,6 +81,14 @@ async def async_setup_entry( config_entry.runtime_data = VeSyncDataCoordinator(hass, config_entry, manager) + # Complete version migration now that we have the account_id + if config_entry.minor_version == 2: + hass.config_entries.async_update_entry( + config_entry, + unique_id=manager.account_id, + minor_version=3, + ) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True @@ -123,7 +131,6 @@ async def async_migrate_entry( else: _LOGGER.debug("Skipping entity with unique_id: %s", reg_entry.unique_id) hass.config_entries.async_update_entry(config_entry, minor_version=2) - return True diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index bc1a47be712..d209e194e58 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -30,7 +30,7 @@ class VeSyncFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - MINOR_VERSION = 2 + MINOR_VERSION = 3 @callback def _show_form(self, errors: dict[str, str] | None = None) -> ConfigFlowResult: @@ -45,8 +45,6 @@ class VeSyncFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow start.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") if not user_input: return self._show_form() @@ -68,6 +66,9 @@ class VeSyncFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.error("VeSync login failed: %s", str(e)) return self._show_form(errors={"base": "invalid_auth"}) + await self.async_set_unique_id(manager.account_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password}, @@ -107,8 +108,12 @@ class VeSyncFlowHandler(ConfigFlow, domain=DOMAIN): errors={"base": "invalid_auth"}, ) + await self.async_set_unique_id(manager.account_id) + self._abort_if_unique_id_mismatch(reason="wrong_account") + return self.async_update_reload_and_abort( self._get_reauth_entry(), + unique_id=manager.account_id, data_updates={ CONF_USERNAME: username, CONF_PASSWORD: password, diff --git a/homeassistant/components/vesync/quality_scale.yaml b/homeassistant/components/vesync/quality_scale.yaml index c4ac491f2b0..d39090dfd59 100644 --- a/homeassistant/components/vesync/quality_scale.yaml +++ b/homeassistant/components/vesync/quality_scale.yaml @@ -19,9 +19,7 @@ rules: runtime-data: done test-before-configure: done test-before-setup: done - unique-config-entry: - status: todo - comment: Can be migrated to accept multiple config entries now + unique-config-entry: done # Silver action-exceptions: todo diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index 115a760876a..4f628f4675f 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -1,8 +1,9 @@ { "config": { "abort": { + "already_configured": "VeSync account is already configured", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "wrong_account": "The account you are trying to re-authenticate is different from the one configured" }, "error": { "api_response_error": "API response error", diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index e8603b37170..d129d696999 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -88,6 +88,9 @@ def config_entry_fixture(hass: HomeAssistant, config) -> ConfigEntry: title="VeSync", domain=DOMAIN, data=config[DOMAIN], + unique_id="TESTACCOUNTID", + version=1, + minor_version=3, ) entry.add_to_hass(hass) return entry @@ -278,6 +281,9 @@ async def humidifier_config_entry( title="VeSync", domain=DOMAIN, data=config[DOMAIN], + unique_id="TESTACCOUNTID", + version=1, + minor_version=3, ) entry.add_to_hass(hass) @@ -313,6 +319,9 @@ async def fan_config_entry( title="VeSync", domain=DOMAIN, data=config[DOMAIN], + unique_id="TESTACCOUNTID", + version=1, + minor_version=3, ) entry.add_to_hass(hass) diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index 4eb41d8f24c..0ff77e46225 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,10 +1,11 @@ """Test for vesync config flow.""" -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from pyvesync.utils.errors import VeSyncLoginError -from homeassistant.components.vesync import DOMAIN, config_flow +from homeassistant.components.vesync import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -12,29 +13,46 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_abort_already_setup(hass: HomeAssistant) -> None: - """Test if we abort because component is already setup.""" - flow = config_flow.VeSyncFlowHandler() - flow.hass = hass - MockConfigEntry(domain=DOMAIN, title="user", data={"user": "pass"}).add_to_hass( - hass +async def test_abort_duplicate_unique_id(hass: HomeAssistant, config_entry) -> None: + """Test if we abort because component is already setup under that Account ID.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} ) - result = await flow.async_step_user() + assert result["type"] is FlowResultType.FORM + + with ( + patch("pyvesync.vesync.VeSync.login"), + patch( + "pyvesync.vesync.VeSync.account_id", new_callable=PropertyMock + ) as mock_account_id, + ): + mock_account_id.return_value = "TESTACCOUNTID" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "user@user.com", CONF_PASSWORD: "pass"}, + ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == "already_configured" async def test_invalid_login_error(hass: HomeAssistant) -> None: """Test if we return error for invalid username and password.""" - test_dict = {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} - flow = config_flow.VeSyncFlowHandler() - flow.hass = hass + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] is FlowResultType.FORM + with patch( "pyvesync.vesync.VeSync.login", side_effect=VeSyncLoginError("Mock login failed"), ): - result = await flow.async_step_user(user_input=test_dict) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "user", CONF_PASSWORD: "pass"}, + ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -42,24 +60,28 @@ async def test_invalid_login_error(hass: HomeAssistant) -> None: async def test_config_flow_user_input(hass: HomeAssistant) -> None: """Test config flow with user input.""" - flow = config_flow.VeSyncFlowHandler() - flow.hass = hass - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) assert result["type"] is FlowResultType.FORM + with patch("pyvesync.vesync.VeSync.login"): - result = await flow.async_step_user( - {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "user", CONF_PASSWORD: "pass"}, ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"][CONF_USERNAME] == "user" - assert result["data"][CONF_PASSWORD] == "pass" + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_USERNAME] == "user" + assert result["data"][CONF_PASSWORD] == "pass" + assert result["result"].unique_id == "TESTACCOUNTID" async def test_reauth_flow(hass: HomeAssistant) -> None: """Test a successful reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - unique_id="test-username", + unique_id="account_id", ) mock_entry.add_to_hass(hass) @@ -67,7 +89,15 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth_confirm" assert result["type"] is FlowResultType.FORM - with patch("pyvesync.vesync.VeSync.login"): + with ( + patch("pyvesync.vesync.VeSync") as mock_vesync, + patch( + "pyvesync.auth.VeSyncAuth._account_id", new_callable=PropertyMock + ) as mock_account_id, + ): + instance = mock_vesync.return_value + instance.login.return_value = None + mock_account_id.return_value = "account_id" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, @@ -86,7 +116,7 @@ async def test_reauth_flow_invalid_auth(hass: HomeAssistant) -> None: mock_entry = MockConfigEntry( domain=DOMAIN, - unique_id="test-username", + unique_id="account_id", ) mock_entry.add_to_hass(hass) @@ -104,7 +134,15 @@ async def test_reauth_flow_invalid_auth(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.FORM - with patch("pyvesync.vesync.VeSync.login"): + with ( + patch("pyvesync.vesync.VeSync") as mock_vesync, + patch( + "pyvesync.auth.VeSyncAuth._account_id", new_callable=PropertyMock + ) as mock_account_id, + ): + instance = mock_vesync.return_value + instance.login.return_value = None + mock_account_id.return_value = "account_id" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_USERNAME: "new-username", CONF_PASSWORD: "new-password"}, diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index 5886f19dd2b..ee9c5caf17e 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -129,7 +129,7 @@ async def test_migrate_config_entry( await hass.config_entries.async_setup(switch_old_id_config_entry.entry_id) await hass.async_block_till_done() - assert switch_old_id_config_entry.minor_version == 2 + assert switch_old_id_config_entry.minor_version == 3 migrated_switch = entity_registry.async_get(switch.entity_id) assert migrated_switch is not None @@ -150,6 +150,8 @@ async def test_migrate_config_entry( e for e in entity_registry.entities.values() if e.domain == "humidifier" ] assert len(humidifier_entities) == 2 + assert switch_old_id_config_entry.version == 1 + assert switch_old_id_config_entry.unique_id == "TESTACCOUNTID" async def test_async_remove_config_entry_device_positive(