diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 781f81ab745..17e4d3dd82b 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -4,7 +4,7 @@ import aiohttp from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_LOCALE, DOMAIN, PLATFORMS from .renault_hub import RenaultHub @@ -22,7 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryNotReady() from exc if not login_success: - return False + raise ConfigEntryAuthFailed() hass.data.setdefault(DOMAIN, {}) await renault_hub.async_initialise(config_entry) diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py index 09a69f1f95f..47832cdbe93 100644 --- a/homeassistant/components/renault/config_flow.py +++ b/homeassistant/components/renault/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure Renault component.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from renault_api.const import AVAILABLE_LOCALES import voluptuous as vol @@ -21,6 +21,7 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Renault config flow.""" + self._original_data: dict[str, Any] | None = None self.renault_config: dict[str, Any] = {} self.renault_hub: RenaultHub | None = None @@ -90,3 +91,51 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): {vol.Required(CONF_KAMEREON_ACCOUNT_ID): vol.In(accounts)} ), ) + + async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._original_data = user_input.copy() + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + if not user_input: + return self._show_reauth_confirm_form() + + if TYPE_CHECKING: + assert self._original_data + + # Check credentials + self.renault_hub = RenaultHub(self.hass, self._original_data[CONF_LOCALE]) + if not await self.renault_hub.attempt_login( + self._original_data[CONF_USERNAME], user_input[CONF_PASSWORD] + ): + return self._show_reauth_confirm_form({"base": "invalid_credentials"}) + + # Update existing entry + data = {**self._original_data, CONF_PASSWORD: user_input[CONF_PASSWORD]} + existing_entry = await self.async_set_unique_id( + self._original_data[CONF_KAMEREON_ACCOUNT_ID] + ) + if TYPE_CHECKING: + assert existing_entry + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + def _show_reauth_confirm_form( + self, errors: dict[str, Any] | None = None + ) -> FlowResult: + """Show the API keys form.""" + if TYPE_CHECKING: + assert self._original_data + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors or {}, + description_placeholders={ + CONF_USERNAME: self._original_data[CONF_USERNAME] + }, + ) diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 942c8b4a06c..30a356b7c42 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "kamereon_no_account": "Unable to find Kamereon account." + "kamereon_no_account": "Unable to find Kamereon account", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]" @@ -14,6 +15,13 @@ }, "title": "Select Kamereon account id" }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Please update your password for {username}", + "title": "[%key:common::config_flow::title::reauth%]" + }, "user": { "data": { "locale": "Locale", diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index 684e17a0101..ebf458541f0 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from . import get_mock_config_entry +from .const import MOCK_CONFIG from tests.common import load_fixture @@ -207,3 +208,54 @@ async def test_config_flow_duplicate(hass: HomeAssistant): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauth(hass): + """Test the start of the config flow.""" + with patch( + "homeassistant.components.renault.async_setup_entry", + return_value=True, + ): + original_entry = get_mock_config_entry() + original_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": original_entry.entry_id, + "unique_id": original_entry.unique_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} + assert result["errors"] == {} + + # Failed credentials + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException( + 403042, "invalid loginID or password" + ), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "any"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} + assert result2["errors"] == {"base": "invalid_credentials"} + + # Valid credentials + with patch("renault_api.renault_session.RenaultSession.login"): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "any"}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "reauth_successful"