From 16a1a9333288e5aaf2d580b8c1712f17254b3f51 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 9 Apr 2022 23:01:48 -0700 Subject: [PATCH] Handle expired credentials in reauth in google calendar initialization (#69772) Co-authored-by: Martin Hjelmare --- homeassistant/components/google/__init__.py | 14 ++++- .../components/google/config_flow.py | 2 +- homeassistant/components/google/strings.json | 2 +- .../components/google/translations/en.json | 4 +- tests/components/google/test_init.py | 52 +++++++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 3df1cf194f05f1..f6629cb3938d87 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -7,6 +7,7 @@ import logging from typing import Any +import aiohttp from httplib2.error import ServerNotFoundError from oauth2client.file import Storage import voluptuous as vol @@ -24,7 +25,11 @@ CONF_OFFSET, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -191,7 +196,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if session.token["expires_at"] >= datetime(2070, 1, 1).timestamp(): session.token["expires_in"] = 0 session.token["expires_at"] = datetime.now().timestamp() + try: await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed from err + raise ConfigEntryNotReady from err + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope if required_scope not in session.token.get("scope", []): diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index c70dd83fcaecab..8bbd2a6c2b141c 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -34,7 +34,7 @@ def logger(self) -> logging.Logger: return logging.getLogger(__name__) async def async_step_import(self, info: dict[str, Any]) -> FlowResult: - """Import existing auth from Nest.""" + """Import existing auth into a new config entry.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") implementations = await config_entry_oauth2_flow.async_get_implementations( diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 14f020f08fd5f9..e8ec7091030516 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -6,7 +6,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Nest integration needs to re-authenticate your account" + "description": "The Google Calendar integration needs to re-authenticate your account" }, "auth": { "title": "Link Google Account" diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 51d1ad9aab8b05..02c8e6d7029184 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is already configured", "already_in_progress": "Configuration flow is already in progress", - "code_expired": "Authentication code expired, please try again.", + "code_expired": "Authentication code expired or credential setup is invalid, please try again.", "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", "oauth_error": "Received invalid token data.", @@ -23,7 +23,7 @@ "title": "Pick Authentication Method" }, "reauth_confirm": { - "description": "The Nest integration needs to re-authenticate your account", + "description": "The Google Calendar integration needs to re-authenticate your account", "title": "Reauthenticate Integration" } } diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 67150de0bcd385..85803c3958ef73 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -3,6 +3,7 @@ from collections.abc import Awaitable, Callable import datetime +import http import time from typing import Any from unittest.mock import Mock, call, patch @@ -32,6 +33,8 @@ from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker +EXPIRED_TOKEN_TIMESTAMP = datetime.datetime(2022, 4, 8).timestamp() + # Typing helpers HassApi = Callable[[], Awaitable[dict[str, Any]]] @@ -505,3 +508,52 @@ async def test_invalid_token_expiry_in_config_entry( assert entries[0].state is ConfigEntryState.LOADED assert entries[0].data["token"]["access_token"] == "some-updated-token" assert entries[0].data["token"]["expires_in"] == expires_in + + +@pytest.mark.parametrize("config_entry_token_expiry", [EXPIRED_TOKEN_TIMESTAMP]) +async def test_expired_token_refresh_internal_error( + hass: HomeAssistant, + component_setup: ComponentSetup, + setup_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Generic errors on reauth are treated as a retryable setup error.""" + + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + status=http.HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + assert await component_setup() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + "config_entry_token_expiry", + [EXPIRED_TOKEN_TIMESTAMP], +) +async def test_expired_token_requires_reauth( + hass: HomeAssistant, + component_setup: ComponentSetup, + setup_config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test case where reauth is required for token that cannot be refreshed.""" + + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + status=http.HTTPStatus.BAD_REQUEST, + ) + + assert await component_setup() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm"