diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index a264ec24389..c640dd2528f 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -13,10 +13,10 @@ from pymazda import ( MazdaTokenExpiredException, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -49,15 +49,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await mazda_client.validate_credentials() - except MazdaAuthenticationException: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False + except MazdaAuthenticationException as ex: + raise ConfigEntryAuthFailed from ex except ( MazdaException, MazdaAccountLockedException, @@ -83,14 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return vehicles except MazdaAuthenticationException as ex: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - raise UpdateFailed("Not authenticated with Mazda API") from ex + raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex except Exception as ex: _LOGGER.exception( "Unknown error occurred during Mazda update request: %s", ex diff --git a/homeassistant/components/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py index 3c1137b8e80..dc4300d2e4d 100644 --- a/homeassistant/components/mazda/config_flow.py +++ b/homeassistant/components/mazda/config_flow.py @@ -32,12 +32,23 @@ class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Start the mazda config flow.""" + self._reauth_entry = None + self._email = None + self._region = None + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) + self._email = user_input[CONF_EMAIL] + self._region = user_input[CONF_REGION] + unique_id = user_input[CONF_EMAIL].lower() + await self.async_set_unique_id(unique_id) + if not self._reauth_entry: + self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) mazda_client = MazdaAPI( user_input[CONF_EMAIL], @@ -60,56 +71,38 @@ class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "Unknown error occurred during Mazda login request: %s", ex ) else: - return self.async_create_entry( - title=user_input[CONF_EMAIL], data=user_input + if not self._reauth_entry: + return self.async_create_entry( + title=user_input[CONF_EMAIL], data=user_input + ) + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input, unique_id=unique_id ) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + 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 + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL, default=self._email): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION, default=self._region): vol.In( + MAZDA_REGIONS + ), + } + ), + errors=errors, ) async def async_step_reauth(self, user_input=None): """Perform reauth if the user credentials have changed.""" - errors = {} - - if user_input is not None: - try: - websession = aiohttp_client.async_get_clientsession(self.hass) - mazda_client = MazdaAPI( - user_input[CONF_EMAIL], - user_input[CONF_PASSWORD], - user_input[CONF_REGION], - websession, - ) - await mazda_client.validate_credentials() - except MazdaAuthenticationException: - errors["base"] = "invalid_auth" - except MazdaAccountLockedException: - errors["base"] = "account_locked" - except aiohttp.ClientError: - errors["base"] = "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - errors["base"] = "unknown" - _LOGGER.exception( - "Unknown error occurred during Mazda login request: %s", ex - ) - else: - await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) - - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) - - # Reload the config entry otherwise devices will remain unavailable - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - - return self.async_abort(reason="reauth_successful") - errors["base"] = "unknown" - - return self.async_show_form( - step_id="reauth", data_schema=DATA_SCHEMA, errors=errors + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] ) + self._email = user_input[CONF_EMAIL] + self._region = user_input[CONF_REGION] + return await self.async_step_user() diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index 1950260bfcb..a7bed8725af 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -11,15 +11,6 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { - "reauth": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "Region" - }, - "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", - "title": "Mazda Connected Services - Authentication Failed" - }, "user": { "data": { "email": "[%key:common::config_flow::data::email%]", diff --git a/homeassistant/components/mazda/translations/en.json b/homeassistant/components/mazda/translations/en.json index b9e02fb3a41..b483947aaa0 100644 --- a/homeassistant/components/mazda/translations/en.json +++ b/homeassistant/components/mazda/translations/en.json @@ -11,15 +11,6 @@ "unknown": "Unexpected error" }, "step": { - "reauth": { - "data": { - "email": "Email", - "password": "Password", - "region": "Region" - }, - "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", - "title": "Mazda Connected Services - Authentication Failed" - }, "user": { "data": { "email": "Email", diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index f4bdfa930bd..06cb0e15d09 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -24,6 +24,11 @@ FIXTURE_USER_INPUT_REAUTH = { CONF_PASSWORD: "password_fixed", CONF_REGION: "MNAO", } +FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL = { + CONF_EMAIL: "example2@example.com", + CONF_PASSWORD: "password_fixed", + CONF_REGION: "MNAO", +} async def test_form(hass): @@ -54,6 +59,36 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_account_already_exists(hass): + """Test account already exists.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( @@ -145,37 +180,40 @@ async def test_form_unknown_error(hass): async def test_reauth_flow(hass: HomeAssistant) -> None: """Test reauth works.""" await setup.async_setup_component(hass, "persistent_notification", {}) + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=MazdaAuthenticationException("Failed to authenticate"), + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): - mock_config = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], - data=FIXTURE_USER_INPUT, - ) - mock_config.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "invalid_auth"} + assert result["step_id"] == "user" + assert result["errors"] == {} with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", return_value=True, - ): - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "reauth", "unique_id": FIXTURE_USER_INPUT[CONF_EMAIL]}, - data=FIXTURE_USER_INPUT_REAUTH, + ), patch("homeassistant.components.mazda.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH, ) await hass.async_block_till_done() @@ -185,16 +223,28 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: async def test_reauth_authorization_error(hass: HomeAssistant) -> None: """Test we show user form on authorization error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=MazdaAuthenticationException("Failed to authenticate"), + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -203,22 +253,34 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} async def test_reauth_account_locked(hass: HomeAssistant) -> None: """Test we show user form on account_locked error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=MazdaAccountLockedException("Account locked"), + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -227,22 +289,34 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} async def test_reauth_connection_error(hass: HomeAssistant) -> None: """Test we show user form on connection error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=aiohttp.ClientError, + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -251,50 +325,34 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} async def test_reauth_unknown_error(hass: HomeAssistant) -> None: """Test we show user form on unknown error.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + with patch( "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", side_effect=Exception, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - FIXTURE_USER_INPUT_REAUTH, - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" - assert result2["errors"] == {"base": "unknown"} - - -async def test_reauth_unique_id_not_found(hass: HomeAssistant) -> None: - """Test we show user form when unique id not found during reauth.""" - with patch( - "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + ), patch( + "homeassistant.components.mazda.async_setup_entry", return_value=True, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - - # Change the unique_id of the flow in order to cause a mismatch - flows = hass.config_entries.flow.async_progress() - flows[0]["context"]["unique_id"] = "example2@example.com" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -303,5 +361,45 @@ async def test_reauth_unique_id_not_found(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "reauth" + assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: + """Test reauth with a new email address but same account.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], + data=FIXTURE_USER_INPUT, + ) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.mazda.config_flow.MazdaAPI.validate_credentials", + return_value=True, + ), patch( + "homeassistant.components.mazda.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth", "entry_id": mock_config.entry_id}, + data=FIXTURE_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # Change the email and ensure the entry and its unique id gets + # updated in the event the user has changed their email with mazda + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL, + ) + await hass.async_block_till_done() + + assert ( + mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL] + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index fe5b96096f1..1b062dd84f1 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -60,7 +60,7 @@ async def test_init_auth_failure(hass: HomeAssistant): flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth" + assert flows[0]["step_id"] == "user" async def test_update_auth_failure(hass: HomeAssistant): @@ -99,7 +99,7 @@ async def test_update_auth_failure(hass: HomeAssistant): flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth" + assert flows[0]["step_id"] == "user" async def test_unload_config_entry(hass: HomeAssistant) -> None: