Handle expired credentials in reauth in google calendar initialization (#69772)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter 2022-04-09 23:01:48 -07:00 committed by GitHub
parent 60681a3800
commit a063f55c82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 69 additions and 5 deletions

View file

@ -7,6 +7,7 @@ from datetime import datetime, timedelta
import logging import logging
from typing import Any from typing import Any
import aiohttp
from httplib2.error import ServerNotFoundError from httplib2.error import ServerNotFoundError
from oauth2client.file import Storage from oauth2client.file import Storage
import voluptuous as vol import voluptuous as vol
@ -24,7 +25,11 @@ from homeassistant.const import (
CONF_OFFSET, CONF_OFFSET,
) )
from homeassistant.core import HomeAssistant, ServiceCall 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 from homeassistant.helpers import config_entry_oauth2_flow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send 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(): if session.token["expires_at"] >= datetime(2070, 1, 1).timestamp():
session.token["expires_in"] = 0 session.token["expires_in"] = 0
session.token["expires_at"] = datetime.now().timestamp() session.token["expires_at"] = datetime.now().timestamp()
try:
await session.async_ensure_token_valid() 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 required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope
if required_scope not in session.token.get("scope", []): if required_scope not in session.token.get("scope", []):

View file

@ -34,7 +34,7 @@ class OAuth2FlowHandler(
return logging.getLogger(__name__) return logging.getLogger(__name__)
async def async_step_import(self, info: dict[str, Any]) -> FlowResult: 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(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
implementations = await config_entry_oauth2_flow.async_get_implementations( implementations = await config_entry_oauth2_flow.async_get_implementations(

View file

@ -6,7 +6,7 @@
}, },
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "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": { "auth": {
"title": "Link Google Account" "title": "Link Google Account"

View file

@ -3,7 +3,7 @@
"abort": { "abort": {
"already_configured": "Account is already configured", "already_configured": "Account is already configured",
"already_in_progress": "Configuration flow is already in progress", "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", "invalid_access_token": "Invalid access token",
"missing_configuration": "The component is not configured. Please follow the documentation.", "missing_configuration": "The component is not configured. Please follow the documentation.",
"oauth_error": "Received invalid token data.", "oauth_error": "Received invalid token data.",
@ -23,7 +23,7 @@
"title": "Pick Authentication Method" "title": "Pick Authentication Method"
}, },
"reauth_confirm": { "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" "title": "Reauthenticate Integration"
} }
} }

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import datetime import datetime
import http
import time import time
from typing import Any from typing import Any
from unittest.mock import Mock, call, patch from unittest.mock import Mock, call, patch
@ -32,6 +33,8 @@ from .conftest import (
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
EXPIRED_TOKEN_TIMESTAMP = datetime.datetime(2022, 4, 8).timestamp()
# Typing helpers # Typing helpers
HassApi = Callable[[], Awaitable[dict[str, Any]]] HassApi = Callable[[], Awaitable[dict[str, Any]]]
@ -507,3 +510,52 @@ async def test_invalid_token_expiry_in_config_entry(
assert entries[0].state is ConfigEntryState.LOADED assert entries[0].state is ConfigEntryState.LOADED
assert entries[0].data["token"]["access_token"] == "some-updated-token" assert entries[0].data["token"]["access_token"] == "some-updated-token"
assert entries[0].data["token"]["expires_in"] == expires_in 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"