From 3027b848c1b956feb55b237d7866c104895cbafd Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 28 Jun 2021 15:01:31 +0200 Subject: [PATCH] Add reauth config flow to devolo Home Control (#49697) --- .../devolo_home_control/__init__.py | 4 +- .../devolo_home_control/config_flow.py | 67 ++++++++--- .../devolo_home_control/exceptions.py | 4 + .../devolo_home_control/strings.json | 6 +- .../devolo_home_control/translations/en.json | 6 +- .../devolo_home_control/test_config_flow.py | 105 +++++++++++++++++- 6 files changed, 170 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index ded30d75de9..4c8757e4eff 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -10,7 +10,7 @@ from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import ( CONF_MYDEVOLO, @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) if not credentials_valid: - return False + raise ConfigEntryAuthFailed if await hass.async_add_executor_job(mydevolo.maintenance): raise ConfigEntryNotReady diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 4f605baf98d..10172b94452 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -8,7 +8,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from . import configure_mydevolo from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN, SUPPORTED_MODEL_TYPES -from .exceptions import CredentialsInvalid +from .exceptions import CredentialsInvalid, UuidChanged class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -22,13 +22,13 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } + self._reauth_entry = None + self._url = DEFAULT_MYDEVOLO async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" if self.show_advanced_options: - self.data_schema[ - vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO) - ] = str + self.data_schema[vol.Required(CONF_MYDEVOLO, default=self._url)] = str if user_input is None: return self._show_form(step_id="user") try: @@ -55,8 +55,36 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="zeroconf_confirm", errors={"base": "invalid_auth"} ) + async def async_step_reauth(self, user_input): + """Handle reauthentication.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + self._url = user_input[CONF_MYDEVOLO] + self.data_schema = { + vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): str, + vol.Required(CONF_PASSWORD): str, + } + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle a flow initiated by reauthentication.""" + if user_input is None: + return self._show_form(step_id="reauth_confirm") + try: + return await self._connect_mydevolo(user_input) + except CredentialsInvalid: + return self._show_form( + step_id="reauth_confirm", errors={"base": "invalid_auth"} + ) + except UuidChanged: + return self._show_form( + step_id="reauth_confirm", errors={"base": "reauth_failed"} + ) + async def _connect_mydevolo(self, user_input): """Connect to mydevolo.""" + user_input[CONF_MYDEVOLO] = user_input.get(CONF_MYDEVOLO, self._url) mydevolo = configure_mydevolo(conf=user_input) credentials_valid = await self.hass.async_add_executor_job( mydevolo.credentials_valid @@ -64,17 +92,30 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not credentials_valid: raise CredentialsInvalid uuid = await self.hass.async_add_executor_job(mydevolo.uuid) - await self.async_set_unique_id(uuid) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title="devolo Home Control", - data={ - CONF_PASSWORD: mydevolo.password, - CONF_USERNAME: mydevolo.user, - CONF_MYDEVOLO: mydevolo.url, - }, + if not self._reauth_entry: + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="devolo Home Control", + data={ + CONF_PASSWORD: mydevolo.password, + CONF_USERNAME: mydevolo.user, + CONF_MYDEVOLO: mydevolo.url, + }, + ) + + if self._reauth_entry.unique_id != uuid: + # The old user and the new user are not the same. This could mess-up everything as all unique IDs might change. + raise UuidChanged + + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input, unique_id=uuid ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") @callback def _show_form(self, step_id, errors=None): diff --git a/homeassistant/components/devolo_home_control/exceptions.py b/homeassistant/components/devolo_home_control/exceptions.py index 378efa41cc5..a89058e6c16 100644 --- a/homeassistant/components/devolo_home_control/exceptions.py +++ b/homeassistant/components/devolo_home_control/exceptions.py @@ -4,3 +4,7 @@ from homeassistant.exceptions import HomeAssistantError class CredentialsInvalid(HomeAssistantError): """Given credentials are invalid.""" + + +class UuidChanged(HomeAssistantError): + """UUID of the user changed.""" diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json index cbc911fcd18..ba1bc20bfd2 100644 --- a/homeassistant/components/devolo_home_control/strings.json +++ b/homeassistant/components/devolo_home_control/strings.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "reauth_failed": "Please use the same mydevolo user as before." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json index d1b8645072f..e5ea6a49cd8 100644 --- a/homeassistant/components/devolo_home_control/translations/en.json +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "reauth_failed": "Please use the same mydevolo user as before." }, "step": { "user": { diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 94435545cc6..054b613f3a0 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -5,7 +5,6 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN -from homeassistant.config_entries import SOURCE_USER from .const import ( DISCOVERY_INFO, @@ -57,7 +56,7 @@ async def test_form_already_configured(hass): MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_USER}, + context={"source": config_entries.SOURCE_USER}, data={"username": "test-username", "password": "test-password"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -70,7 +69,7 @@ async def test_form_advanced_options(hass): DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -157,6 +156,106 @@ async def test_zeroconf_wrong_device(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT +async def test_form_reauth(hass): + """Test that the reauth confirmation form is served.""" + mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + data={ + "username": "test-username", + "password": "test-password", + "mydevolo_url": "https://test_mydevolo_url.test", + }, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="123456", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username-new", "password": "test-password-new"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.credentials_invalid +async def test_form_invalid_credentials_reauth(hass): + """Test if we get the error message on invalid credentials.""" + mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + data={ + "username": "test-username", + "password": "test-password", + "mydevolo_url": "https://test_mydevolo_url.test", + }, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_form_uuid_change_reauth(hass): + """Test that the reauth confirmation form is served.""" + mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + data={ + "username": "test-username", + "password": "test-password", + "mydevolo_url": "https://test_mydevolo_url.test", + }, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="789123", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username-new", "password": "test-password-new"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "reauth_failed"} + + async def _setup(hass, result): """Finish configuration steps.""" with patch(