Set user friendly name for Google Calendar config entry (#72243)

* Set user friendly name for Google Calendar config entry

* Add a new auth implementation for use during the config flow
This commit is contained in:
Allen Porter 2022-05-21 11:22:27 -07:00 committed by GitHub
parent 17669a728c
commit 3f8c896cb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 8 deletions

View File

@ -173,3 +173,25 @@ class ApiAuthImpl(AbstractAuth):
"""Return a valid access token.""" """Return a valid access token."""
await self._session.async_ensure_token_valid() await self._session.async_ensure_token_valid()
return self._session.token["access_token"] return self._session.token["access_token"]
class AccessTokenAuthImpl(AbstractAuth):
"""Authentication implementation used during config flow, without refresh.
This exists to allow the config flow to use the API before it has fully
created a config entry required by OAuth2Session. This does not support
refreshing tokens, which is fine since it should have been just created.
"""
def __init__(
self,
websession: aiohttp.ClientSession,
access_token: str,
) -> None:
"""Init the Google Calendar client library auth implementation."""
super().__init__(websession)
self._access_token = access_token
async def async_get_access_token(self) -> str:
"""Return the access token."""
return self._access_token

View File

@ -4,13 +4,17 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from gcal_sync.api import GoogleCalendarService
from gcal_sync.exceptions import ApiException
from oauth2client.client import Credentials from oauth2client.client import Credentials
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .api import ( from .api import (
DEVICE_AUTH_CREDS, DEVICE_AUTH_CREDS,
AccessTokenAuthImpl,
DeviceAuth, DeviceAuth,
DeviceFlow, DeviceFlow,
OAuthError, OAuthError,
@ -130,7 +134,19 @@ class OAuth2FlowHandler(
self.hass.config_entries.async_update_entry(entry, data=data) self.hass.config_entries.async_update_entry(entry, data=data)
await self.hass.config_entries.async_reload(entry.entry_id) await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=self.flow_impl.name, data=data)
calendar_service = GoogleCalendarService(
AccessTokenAuthImpl(
async_get_clientsession(self.hass), data["token"]["access_token"]
)
)
try:
primary_calendar = await calendar_service.async_get_calendar("primary")
except ApiException as err:
_LOGGER.debug("Error reading calendar primary calendar: %s", err)
primary_calendar = None
title = primary_calendar.id if primary_calendar else self.flow_impl.name
return self.async_create_entry(title=title, data=data)
async def async_step_reauth( async def async_step_reauth(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None

View File

@ -6,6 +6,7 @@ import datetime
from typing import Any, Generator, TypeVar from typing import Any, Generator, TypeVar
from unittest.mock import mock_open, patch from unittest.mock import mock_open, patch
from aiohttp.client_exceptions import ClientError
from gcal_sync.auth import API_BASE_URL from gcal_sync.auth import API_BASE_URL
from oauth2client.client import Credentials, OAuth2Credentials from oauth2client.client import Credentials, OAuth2Credentials
import pytest import pytest
@ -207,7 +208,9 @@ def mock_events_list(
"""Fixture to construct a fake event list API response.""" """Fixture to construct a fake event list API response."""
def _put_result( def _put_result(
response: dict[str, Any], calendar_id: str = None, exc: Exception = None response: dict[str, Any],
calendar_id: str = None,
exc: ClientError | None = None,
) -> None: ) -> None:
if calendar_id is None: if calendar_id is None:
calendar_id = CALENDAR_ID calendar_id = CALENDAR_ID
@ -240,7 +243,7 @@ def mock_calendars_list(
) -> ApiResult: ) -> ApiResult:
"""Fixture to construct a fake calendar list API response.""" """Fixture to construct a fake calendar list API response."""
def _put_result(response: dict[str, Any], exc=None) -> None: def _result(response: dict[str, Any], exc: ClientError | None = None) -> None:
aioclient_mock.get( aioclient_mock.get(
f"{API_BASE_URL}/users/me/calendarList", f"{API_BASE_URL}/users/me/calendarList",
json=response, json=response,
@ -248,13 +251,32 @@ def mock_calendars_list(
) )
return return
return _put_result return _result
@pytest.fixture
def mock_calendar_get(
aioclient_mock: AiohttpClientMocker,
) -> Callable[[...], None]:
"""Fixture for returning a calendar get response."""
def _result(
calendar_id: str, response: dict[str, Any], exc: ClientError | None = None
) -> None:
aioclient_mock.get(
f"{API_BASE_URL}/calendars/{calendar_id}",
json=response,
exc=exc,
)
return
return _result
@pytest.fixture @pytest.fixture
def mock_insert_event( def mock_insert_event(
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
) -> Callable[[..., dict[str, Any]], None]: ) -> Callable[[...], None]:
"""Fixture for capturing event creation.""" """Fixture for capturing event creation."""
def _expect_result(calendar_id: str = CALENDAR_ID) -> None: def _expect_result(calendar_id: str = CALENDAR_ID) -> None:

View File

@ -1,9 +1,13 @@
"""Test the google config flow.""" """Test the google config flow."""
from __future__ import annotations
from collections.abc import Callable
import datetime import datetime
from typing import Any from typing import Any
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from aiohttp.client_exceptions import ClientError
from oauth2client.client import ( from oauth2client.client import (
FlowExchangeError, FlowExchangeError,
OAuth2Credentials, OAuth2Credentials,
@ -27,6 +31,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed
CODE_CHECK_INTERVAL = 1 CODE_CHECK_INTERVAL = 1
CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2)
EMAIL_ADDRESS = "user@gmail.com"
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -63,6 +68,24 @@ async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]:
yield mock yield mock
@pytest.fixture
async def primary_calendar_error() -> ClientError | None:
"""Fixture for tests to inject an error during calendar lookup."""
return None
@pytest.fixture(autouse=True)
async def primary_calendar(
mock_calendar_get: Callable[[...], None], primary_calendar_error: ClientError | None
) -> None:
"""Fixture to return the primary calendar."""
mock_calendar_get(
"primary",
{"id": EMAIL_ADDRESS, "summary": "Personal"},
exc=primary_calendar_error,
)
async def fire_alarm(hass, point_in_time): async def fire_alarm(hass, point_in_time):
"""Fire an alarm and wait for callbacks to run.""" """Fire an alarm and wait for callbacks to run."""
with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): with patch("homeassistant.util.dt.utcnow", return_value=point_in_time):
@ -99,7 +122,7 @@ async def test_full_flow_yaml_creds(
) )
assert result.get("type") == "create_entry" assert result.get("type") == "create_entry"
assert result.get("title") == "Import from configuration.yaml" assert result.get("title") == EMAIL_ADDRESS
assert "data" in result assert "data" in result
data = result["data"] data = result["data"]
assert "token" in data assert "token" in data
@ -161,7 +184,7 @@ async def test_full_flow_application_creds(
) )
assert result.get("type") == "create_entry" assert result.get("type") == "create_entry"
assert result.get("title") == "Import from configuration.yaml" assert result.get("title") == EMAIL_ADDRESS
assert "data" in result assert "data" in result
data = result["data"] data = result["data"]
assert "token" in data assert "token" in data
@ -278,7 +301,7 @@ async def test_exchange_error(
) )
assert result.get("type") == "create_entry" assert result.get("type") == "create_entry"
assert result.get("title") == "Import from configuration.yaml" assert result.get("title") == EMAIL_ADDRESS
assert "data" in result assert "data" in result
data = result["data"] data = result["data"]
assert "token" in data assert "token" in data
@ -463,3 +486,40 @@ async def test_reauth_flow(
} }
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
@pytest.mark.parametrize("primary_calendar_error", [ClientError()])
async def test_title_lookup_failure(
hass: HomeAssistant,
mock_code_flow: Mock,
mock_exchange: Mock,
component_setup: ComponentSetup,
) -> None:
"""Test successful config flow and title fetch fails gracefully."""
assert await component_setup()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result.get("type") == "progress"
assert result.get("step_id") == "auth"
assert "description_placeholders" in result
assert "url" in result["description_placeholders"]
with patch(
"homeassistant.components.google.async_setup_entry", return_value=True
) as mock_setup:
# Run one tick to invoke the credential exchange check
now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_configure(
flow_id=result["flow_id"]
)
assert result.get("type") == "create_entry"
assert result.get("title") == "Import from configuration.yaml"
assert len(mock_setup.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1