From b051704c4b736995d77c4867a13b84c0a4874d05 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 20 Dec 2021 13:11:26 -0700 Subject: [PATCH] Add reauth flow to Tile (#62415) --- homeassistant/components/tile/__init__.py | 9 +- homeassistant/components/tile/config_flow.py | 91 +++++++++++++++---- homeassistant/components/tile/strings.json | 9 +- .../components/tile/translations/en.json | 9 +- tests/components/tile/test_config_flow.py | 55 +++++++++-- 5 files changed, 141 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 388ffbff7fd..0787e00a489 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -11,7 +11,7 @@ from pytile.tile import Tile from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -68,9 +68,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=websession, ) tiles = await client.async_get_tiles() - except InvalidAuthError: - LOGGER.error("Invalid credentials provided") - return False + except InvalidAuthError as err: + raise ConfigEntryAuthFailed("Invalid credentials") from err except TileError as err: raise ConfigEntryNotReady("Error during integration setup") from err @@ -78,6 +77,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Update the Tile.""" try: await tile.async_update() + except InvalidAuthError as err: + raise ConfigEntryAuthFailed("Invalid credentials") from err except SessionExpiredError: LOGGER.info("Tile session expired; creating a new one") await client.async_init() diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index 3c78e5d2bca..58bb929e446 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -4,15 +4,29 @@ from __future__ import annotations from typing import Any from pytile import async_login -from pytile.errors import TileError +from pytile.errors import InvalidAuthError, TileError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, LOGGER + +STEP_REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } +) + +STEP_USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -22,37 +36,74 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self.data_schema = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} - ) + self._password: str | None = None + self._username: str | None = None - async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: - """Show the form to the user.""" - return self.async_show_form( - step_id="user", data_schema=self.data_schema, errors=errors or {} - ) + async def _async_verify(self, step_id: str, schema: vol.Schema) -> FlowResult: + """Attempt to authenticate the provided credentials.""" + assert self._username + assert self._password + + errors = {} + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + await async_login(self._username, self._password, session=session) + except InvalidAuthError: + errors["base"] = "invalid_auth" + except TileError as err: + LOGGER.error("Unknown Tile error: %s", err) + errors["base"] = "unknown" + + if errors: + return self.async_show_form( + step_id=step_id, data_schema=schema, errors=errors + ) + + data = {CONF_USERNAME: self._username, CONF_PASSWORD: self._password} + + if existing_entry := await self.async_set_unique_id(self._username): + self.hass.config_entries.async_update_entry(existing_entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=self._username, data=data) async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self._username = config[CONF_USERNAME] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA + ) + + self._password = user_input[CONF_PASSWORD] + + return await self._async_verify("reauth_confirm", STEP_REAUTH_SCHEMA) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: - return await self._show_form() + return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA) await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() - session = aiohttp_client.async_get_clientsession(self.hass) + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] - try: - await async_login( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=session - ) - except TileError: - return await self._show_form({"base": "invalid_auth"}) - - return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) + return await self._async_verify("user", STEP_USER_SCHEMA) diff --git a/homeassistant/components/tile/strings.json b/homeassistant/components/tile/strings.json index cdddbe54f96..da53f79b697 100644 --- a/homeassistant/components/tile/strings.json +++ b/homeassistant/components/tile/strings.json @@ -1,6 +1,12 @@ { "config": { "step": { + "reauth_confirm": { + "title": "Re-authenticate Tile", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "title": "Configure Tile", "data": { @@ -13,7 +19,8 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "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%]" } }, "options": { diff --git a/homeassistant/components/tile/translations/en.json b/homeassistant/components/tile/translations/en.json index c052cad6d70..998f3a53392 100644 --- a/homeassistant/components/tile/translations/en.json +++ b/homeassistant/components/tile/translations/en.json @@ -1,12 +1,19 @@ { "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" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Re-authenticate Tile" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index e5561133a35..ae86bc4dc7c 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -1,11 +1,12 @@ """Define tests for the Tile config flow.""" from unittest.mock import patch -from pytile.errors import TileError +import pytest +from pytile.errors import InvalidAuthError, TileError from homeassistant import data_entry_flow from homeassistant.components.tile import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -30,8 +31,15 @@ async def test_duplicate_error(hass): assert result["reason"] == "already_configured" -async def test_invalid_credentials(hass): - """Test that invalid credentials key throws an error.""" +@pytest.mark.parametrize( + "err,err_string", + [ + (InvalidAuthError, "invalid_auth"), + (TileError, "unknown"), + ], +) +async def test_errors(hass, err, err_string): + """Test that errors are handled correctly.""" conf = { CONF_USERNAME: "user@host.com", CONF_PASSWORD: "123abc", @@ -39,13 +47,13 @@ async def test_invalid_credentials(hass): with patch( "homeassistant.components.tile.config_flow.async_login", - side_effect=TileError, + side_effect=err, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {"base": err_string} async def test_step_import(hass): @@ -69,6 +77,41 @@ async def test_step_import(hass): } +async def test_step_reauth(hass): + """Test that the reauth step works.""" + conf = { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "123abc", + } + + MockConfigEntry(domain=DOMAIN, unique_id="user@host.com", data=conf).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password"}, + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.tile.async_setup_entry", return_value=True + ), patch("homeassistant.components.tile.config_flow.async_login"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password"} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1 + + async def test_step_user(hass): """Test that the user step works.""" conf = {