Handle expired credentials in reauth in google calendar initialization (#69772)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
60681a3800
commit
a063f55c82
5 changed files with 69 additions and 5 deletions
|
@ -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", []):
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue