From 912d5c347cd90c8e65f73b0bb42c3f334e549d2a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 17 Apr 2021 18:20:16 +0100 Subject: [PATCH] Add reauth flow for lyric (#47863) --- homeassistant/components/lyric/__init__.py | 9 ++- homeassistant/components/lyric/config_flow.py | 24 +++++++ homeassistant/components/lyric/strings.json | 7 +- .../components/lyric/translations/en.json | 7 +- tests/components/lyric/test_config_flow.py | 68 +++++++++++++++++++ 5 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index c3ef18e7c7f..7a6e00da7d2 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -6,7 +6,9 @@ from datetime import timedelta import logging from typing import Any +from aiohttp.client_exceptions import ClientResponseError from aiolyric import Lyric +from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout @@ -15,6 +17,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -29,7 +32,7 @@ from homeassistant.helpers.update_coordinator import ( from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation from .config_flow import OAuth2FlowHandler -from .const import DOMAIN, LYRIC_EXCEPTIONS, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN CONFIG_SCHEMA = vol.Schema( { @@ -94,7 +97,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(60): await lyric.get_locations() return lyric - except LYRIC_EXCEPTIONS as exception: + except LyricAuthenticationException as exception: + raise ConfigEntryAuthFailed from exception + except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 1370d5e67ea..dedd84c4757 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Honeywell Lyric.""" import logging +import voluptuous as vol + from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow @@ -21,3 +23,25 @@ class OAuth2FlowHandler( def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + 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: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an oauth config entry or update existing entry for reauth.""" + existing_entry = await self.async_set_unique_id(DOMAIN) + if 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") + return self.async_create_entry(title="Lyric", data=data) diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 4e5f2330840..3c9cd6043df 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -3,11 +3,16 @@ "step": { "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Lyric integration needs to re-authenticate your account." } }, "abort": { + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json index e3849fc17a3..17586f16109 100644 --- a/homeassistant/components/lyric/translations/en.json +++ b/homeassistant/components/lyric/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", - "missing_configuration": "The component is not configured. Please follow the documentation." + "missing_configuration": "The component is not configured. Please follow the documentation.", + "reauth_successful": "Re-authentication was successful" }, "create_entry": { "default": "Successfully authenticated" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Lyric integration needs to re-authenticate your account.", + "title": "Reauthenticate Integration" } } } diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index bfdd45f0f8e..71fb473127d 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -11,6 +11,8 @@ from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from tests.common import MockConfigEntry + CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -131,3 +133,69 @@ async def test_abort_if_authorization_timeout( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "authorize_url_timeout" + + +async def test_reauthentication_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): + """Test reauthentication flow.""" + await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + }, + DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, + }, + ) + + old_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=DOMAIN, + version=1, + data={"id": "timmo", "auth_implementation": DOMAIN}, + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=old_entry.data + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await aiohttp_client(hass.http.app) + await client.get(f"/auth/external/callback?code=abcd&state={state}") + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"): + with patch( + "homeassistant.components.lyric.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert len(mock_setup.mock_calls) == 1