diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 6a158c60fcf..6c98c389984 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -2,7 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ATTR_MANUFACTURER, DOMAIN @@ -20,8 +20,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b api = await hass.async_add_executor_job(get_api, dict(config_entry.data)) except CannotConnect as api_error: raise ConfigEntryNotReady from api_error - except LoginError: - return False + except LoginError as err: + raise ConfigEntryAuthFailed from err coordinator = MikrotikDataUpdateCoordinator(hass, config_entry, api) await hass.async_add_executor_job(coordinator.api.get_hub_details) diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index ed62734578f..84b334c5f8f 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Mikrotik.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any import voluptuous as vol @@ -33,6 +34,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Mikrotik config flow.""" VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None @staticmethod @callback @@ -76,6 +78,49 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + 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: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + assert self._reauth_entry + if user_input is not None: + user_input = {**self._reauth_entry.data, **user_input} + try: + await self.hass.async_add_executor_job(get_api, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except LoginError: + errors[CONF_PASSWORD] = "invalid_auth" + + if not errors: + 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( + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): """Handle Mikrotik options.""" diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 08320c603f9..26a58948620 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -13,6 +13,7 @@ from librouteros.login import plain as login_plain, token as login_token from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -132,8 +133,10 @@ class MikrotikData: # get new hub firmware version if updated self.firmware = self.get_info(ATTR_FIRMWARE) - except (CannotConnect, LoginError) as err: + except CannotConnect as err: raise UpdateFailed from err + except LoginError as err: + raise ConfigEntryAuthFailed from err if not device_list: return diff --git a/homeassistant/components/mikrotik/strings.json b/homeassistant/components/mikrotik/strings.json index 6d421cb1838..ec47d98b7a9 100644 --- a/homeassistant/components/mikrotik/strings.json +++ b/homeassistant/components/mikrotik/strings.json @@ -11,6 +11,13 @@ "port": "[%key:common::config_flow::data::port%]", "verify_ssl": "Use ssl" } + }, + "reauth_confirm": { + "description": "The password for {username} is invalid.", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -19,7 +26,8 @@ "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_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/mikrotik/translations/en.json b/homeassistant/components/mikrotik/translations/en.json index d60a7064e3a..9874ed21ff1 100644 --- a/homeassistant/components/mikrotik/translations/en.json +++ b/homeassistant/components/mikrotik/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "name_exists": "Name exists" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is invalid.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index 6a71806cea9..6a2945c406b 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -162,3 +162,99 @@ async def test_wrong_credentials(hass, auth_error): CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", } + + +async def test_reauth_success(hass, api): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEMO_USER_INPUT, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=DEMO_USER_INPUT, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {CONF_USERNAME: "username"} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_failed(hass, auth_error): + """Test reauth fails due to wrong password.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEMO_USER_INPUT, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=DEMO_USER_INPUT, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == { + CONF_PASSWORD: "invalid_auth", + } + + +async def test_reauth_failed_conn_error(hass, conn_error): + """Test reauth failed due to connection error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEMO_USER_INPUT, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=DEMO_USER_INPUT, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"}