diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py index d3349060a07..933f5a92367 100644 --- a/homeassistant/components/smarttub/config_flow.py +++ b/homeassistant/components/smarttub/config_flow.py @@ -10,44 +10,84 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from .const import DOMAIN from .controller import SmartTubController +_LOGGER = logging.getLogger(__name__) + + DATA_SCHEMA = vol.Schema( {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} ) -_LOGGER = logging.getLogger(__name__) - - class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """SmartTub configuration flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self) -> None: + """Instantiate config flow.""" + super().__init__() + self._reauth_input = None + self._reauth_entry = None + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} + if user_input is not None: + controller = SmartTubController(self.hass) + try: + account = await controller.login( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + + except LoginFailed: + errors["base"] = "invalid_auth" + else: + await self.async_set_unique_id(account.id) + + if self._reauth_input is None: + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_EMAIL], data=user_input + ) + + # this is a reauth attempt + if self._reauth_entry.unique_id != self.unique_id: + # there is a config entry matching this account, but it is not the one we were trying to reauth + return self.async_abort(reason="already_configured") + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, user_input=None): + """Get new credentials if the current ones don't work anymore.""" + self._reauth_input = dict(user_input) + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" if user_input is None: + # same as DATA_SCHEMA but with default email + data_schema = vol.Schema( + { + vol.Required( + CONF_EMAIL, default=self._reauth_input.get(CONF_EMAIL) + ): str, + vol.Required(CONF_PASSWORD): str, + } + ) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="reauth_confirm", + data_schema=data_schema, ) - - controller = SmartTubController(self.hass) - try: - account = await controller.login( - user_input[CONF_EMAIL], user_input[CONF_PASSWORD] - ) - except LoginFailed: - errors["base"] = "invalid_auth" - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors - ) - - existing_entry = await self.async_set_unique_id(account.id) - if existing_entry: - self.hass.config_entries.async_update_entry(existing_entry, data=user_input) - await self.hass.config_entries.async_reload(existing_entry.entry_id) - return self.async_abort(reason="reauth_successful") - - return self.async_create_entry(title=user_input[CONF_EMAIL], data=user_input) + return await self.async_step_user(user_input) diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 23bd8bd8ec0..ad737bcd63a 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -25,3 +25,5 @@ ATTR_LIGHTS = "lights" ATTR_PUMPS = "pumps" ATTR_REMINDERS = "reminders" ATTR_STATUS = "status" + +CONF_CONFIG_ENTRY = "config_entry" diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 8139c72ab6e..0b395a10fe5 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -10,7 +10,7 @@ from smarttub import APIError, LoginFailed, SmartTub from smarttub.api import Account from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -52,10 +52,9 @@ class SmartTubController: self._account = await self.login( entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD] ) - except LoginFailed: + except LoginFailed as ex: # credentials were changed or invalidated, we need new ones - - return False + raise ConfigEntryAuthFailed from ex except ( asyncio.TimeoutError, client_exceptions.ClientOSError, diff --git a/homeassistant/components/smarttub/strings.json b/homeassistant/components/smarttub/strings.json index 8ba888a9ffb..25528b8a374 100644 --- a/homeassistant/components/smarttub/strings.json +++ b/homeassistant/components/smarttub/strings.json @@ -8,13 +8,17 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The SmartTub integration needs to re-authenticate your account" } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/smarttub/translations/en.json b/homeassistant/components/smarttub/translations/en.json index 4cf93091887..752faa76b95 100644 --- a/homeassistant/components/smarttub/translations/en.json +++ b/homeassistant/components/smarttub/translations/en.json @@ -1,14 +1,17 @@ { "config": { "abort": { - "already_configured": "Device is already configured", + "already_configured": "Account is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "invalid_auth": "Invalid authentication" }, "step": { + "reauth_confirm": { + "description": "The SmartTub integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + }, "user": { "data": { "email": "Email", diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index 8e4d575119e..c6170afc30e 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -3,8 +3,11 @@ from unittest.mock import patch from smarttub import LoginFailed -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.smarttub.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry async def test_form(hass): @@ -19,29 +22,19 @@ async def test_form(hass): "homeassistant.components.smarttub.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"email": "test-email", "password": "test-password"}, + {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result2["type"] == "create_entry" - assert result2["title"] == "test-email" - assert result2["data"] == { - "email": "test-email", - "password": "test-password", - } - await hass.async_block_till_done() - mock_setup_entry.assert_called_once() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"email": "test-email2", "password": "test-password2"} - ) - assert result2["type"] == "abort" - assert result2["reason"] == "reauth_successful" + assert result["type"] == "create_entry" + assert result["title"] == "test-email" + assert result["data"] == { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + } + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() async def test_form_invalid_auth(hass, smarttub_api): @@ -52,10 +45,81 @@ async def test_form_invalid_auth(hass, smarttub_api): smarttub_api.login.side_effect = LoginFailed - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"email": "test-email", "password": "test-password"}, + {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_success(hass, smarttub_api, account): + """Test reauthentication flow.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"}, + unique_id=account.id, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_EMAIL: "test-email3", CONF_PASSWORD: "test-password3"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert mock_entry.data[CONF_EMAIL] == "test-email3" + assert mock_entry.data[CONF_PASSWORD] == "test-password3" + + +async def test_reauth_wrong_account(hass, smarttub_api, account): + """Test reauthentication flow if the user enters credentials for a different already-configured account.""" + mock_entry1 = MockConfigEntry( + domain=DOMAIN, + data={CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"}, + unique_id=account.id, + ) + mock_entry1.add_to_hass(hass) + + mock_entry2 = MockConfigEntry( + domain=DOMAIN, + data={CONF_EMAIL: "test-email2", CONF_PASSWORD: "test-password2"}, + unique_id="mockaccount2", + ) + mock_entry2.add_to_hass(hass) + + # we try to reauth account #2, and the user successfully authenticates to account #1 + account.id = mock_entry1.unique_id + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry2.unique_id, + "entry_id": mock_entry2.entry_id, + }, + data=mock_entry2.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index 01989818d3b..df44edb3da3 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -1,13 +1,16 @@ """Test smarttub setup process.""" import asyncio +from unittest.mock import patch from smarttub import LoginFailed from homeassistant.components import smarttub +from homeassistant.components.smarttub.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, + SOURCE_REAUTH, ) from homeassistant.setup import async_setup_component @@ -35,8 +38,18 @@ async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_a smarttub_api.login.side_effect = LoginFailed config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + mock_flow_init.assert_called_with( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + data=config_entry.data, + ) async def test_config_passed_to_config_entry(hass, config_entry, config_data):