Fix Google Mail expired authorization (#102735)

* Fix Google Mail expired authorization

* add test

* raise HomeAssistantError

* handle in api module

* uno mas
This commit is contained in:
Robert Hillis 2023-10-30 07:36:34 -04:00 committed by GitHub
parent ba7dbc5927
commit 7dbe0c3a48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 22 deletions

View file

@ -1,12 +1,9 @@
"""Support for Google Mail.""" """Support for Google Mail."""
from __future__ import annotations from __future__ import annotations
from aiohttp.client_exceptions import ClientError, ClientResponseError
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_NAME, Platform from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.config_entry_oauth2_flow import ( from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session, OAuth2Session,
@ -35,16 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
implementation = await async_get_config_entry_implementation(hass, entry) implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation) session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(session) auth = AsyncConfigEntryAuth(session)
try: await auth.check_and_refresh_token()
await auth.check_and_refresh_token()
except ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
"OAuth session is not valid, reauth required"
) from err
raise ConfigEntryNotReady from err
except ClientError as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN][entry.entry_id] = auth hass.data[DOMAIN][entry.entry_id] = auth
hass.async_create_task( hass.async_create_task(

View file

@ -1,9 +1,16 @@
"""API for Google Mail bound to Home Assistant OAuth.""" """API for Google Mail bound to Home Assistant OAuth."""
from aiohttp.client_exceptions import ClientError, ClientResponseError
from google.auth.exceptions import RefreshError from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials from google.oauth2.credentials import Credentials
from googleapiclient.discovery import Resource, build from googleapiclient.discovery import Resource, build
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
@ -24,14 +31,30 @@ class AsyncConfigEntryAuth:
async def check_and_refresh_token(self) -> str: async def check_and_refresh_token(self) -> str:
"""Check the token.""" """Check the token."""
await self.oauth_session.async_ensure_token_valid() try:
await self.oauth_session.async_ensure_token_valid()
except (RefreshError, ClientResponseError, ClientError) as ex:
if (
self.oauth_session.config_entry.state
is ConfigEntryState.SETUP_IN_PROGRESS
):
if isinstance(ex, ClientResponseError) and 400 <= ex.status < 500:
raise ConfigEntryAuthFailed(
"OAuth session is not valid, reauth required"
) from ex
raise ConfigEntryNotReady from ex
if (
isinstance(ex, RefreshError)
or hasattr(ex, "status")
and ex.status == 400
):
self.oauth_session.config_entry.async_start_reauth(
self.oauth_session.hass
)
raise HomeAssistantError(ex) from ex
return self.access_token return self.access_token
async def get_resource(self) -> Resource: async def get_resource(self) -> Resource:
"""Get current resource.""" """Get current resource."""
try: credentials = Credentials(await self.check_and_refresh_token())
credentials = Credentials(await self.check_and_refresh_token())
except RefreshError as ex:
self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass)
raise ex
return build("gmail", "v1", credentials=credentials) return build("gmail", "v1", credentials=credentials)

View file

@ -73,8 +73,13 @@ async def test_expired_token_refresh_success(
http.HTTPStatus.INTERNAL_SERVER_ERROR, http.HTTPStatus.INTERNAL_SERVER_ERROR,
ConfigEntryState.SETUP_RETRY, ConfigEntryState.SETUP_RETRY,
), ),
(
time.time() - 3600,
http.HTTPStatus.BAD_REQUEST,
ConfigEntryState.SETUP_ERROR,
),
], ],
ids=["failure_requires_reauth", "transient_failure"], ids=["failure_requires_reauth", "transient_failure", "revoked_auth"],
) )
async def test_expired_token_refresh_failure( async def test_expired_token_refresh_failure(
hass: HomeAssistant, hass: HomeAssistant,

View file

@ -1,12 +1,14 @@
"""Services tests for the Google Mail integration.""" """Services tests for the Google Mail integration."""
from unittest.mock import patch from unittest.mock import patch
from aiohttp.client_exceptions import ClientResponseError
from google.auth.exceptions import RefreshError from google.auth.exceptions import RefreshError
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.google_mail import DOMAIN from homeassistant.components.google_mail import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .conftest import BUILD, SENSOR, TOKEN, ComponentSetup from .conftest import BUILD, SENSOR, TOKEN, ComponentSetup
@ -57,13 +59,22 @@ async def test_set_vacation(
assert len(mock_client.mock_calls) == 5 assert len(mock_client.mock_calls) == 5
@pytest.mark.parametrize(
("side_effect"),
(
(RefreshError,),
(ClientResponseError("", (), status=400),),
),
)
async def test_reauth_trigger( async def test_reauth_trigger(
hass: HomeAssistant, setup_integration: ComponentSetup hass: HomeAssistant,
setup_integration: ComponentSetup,
side_effect,
) -> None: ) -> None:
"""Test reauth is triggered after a refresh error during service call.""" """Test reauth is triggered after a refresh error during service call."""
await setup_integration() await setup_integration()
with patch(TOKEN, side_effect=RefreshError), pytest.raises(RefreshError): with patch(TOKEN, side_effect=side_effect), pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
"set_vacation", "set_vacation",