1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 04:50:05 +00:00

Update google calendar integration with a config flow (#68010)

* Convert google calendar to config flow and async

* Call correct exchange method

* Fix async method and reduce unnecessary diffs

* Wording improvements

* Reduce unnecessary diffs

* Run load/update config from executor

* Update homeassistant/components/google/calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Remove unnecessary updating of unexpected multiple config entries.

* Remove unnecessary unique_id checks

* Improve readability with comments about device code expiration

* Update homeassistant/components/google/calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/google/calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/google/api.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add comment for when code is none on timeout

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter
2022-03-14 23:51:02 -07:00
committed by GitHub
parent 4988c4683c
commit 7876ffe9e3
13 changed files with 989 additions and 443 deletions

View File

@@ -6,11 +6,6 @@ import datetime
from typing import Any
from unittest.mock import Mock, call, patch
from oauth2client.client import (
FlowExchangeError,
OAuth2Credentials,
OAuth2DeviceCodeError,
)
import pytest
from homeassistant.components.google import (
@@ -18,6 +13,7 @@ from homeassistant.components.google import (
SERVICE_ADD_EVENT,
SERVICE_SCAN_CALENDARS,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant, State
from homeassistant.util.dt import utcnow
@@ -30,73 +26,13 @@ from .conftest import (
TEST_YAML_ENTITY_NAME,
ApiResult,
ComponentSetup,
YieldFixture,
)
from tests.common import async_fire_time_changed
from tests.common import MockConfigEntry
# Typing helpers
HassApi = Callable[[], Awaitable[dict[str, Any]]]
CODE_CHECK_INTERVAL = 1
CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2)
@pytest.fixture
async def code_expiration_delta() -> datetime.timedelta:
"""Fixture for code expiration time, defaulting to the future."""
return datetime.timedelta(minutes=3)
@pytest.fixture
async def mock_code_flow(
code_expiration_delta: datetime.timedelta,
) -> YieldFixture[Mock]:
"""Fixture for initiating OAuth flow."""
with patch(
"oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes",
) as mock_flow:
mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta
mock_flow.return_value.interval = CODE_CHECK_INTERVAL
yield mock_flow
@pytest.fixture
async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]:
"""Fixture for mocking out the exchange for credentials."""
with patch(
"oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds
) as mock:
yield mock
@pytest.fixture
async def mock_notification() -> YieldFixture[Mock]:
"""Fixture for capturing persistent notifications."""
with patch("homeassistant.components.persistent_notification.create") as mock:
yield mock
async def fire_alarm(hass, point_in_time):
"""Fire an alarm and wait for callbacks to run."""
with patch("homeassistant.util.dt.utcnow", return_value=point_in_time):
async_fire_time_changed(hass, point_in_time)
await hass.async_block_till_done()
@pytest.mark.parametrize("config", [{}])
async def test_setup_config_empty(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_notification: Mock,
):
"""Test setup component with an empty configuruation."""
assert await component_setup()
mock_notification.assert_not_called()
assert not hass.states.get(TEST_YAML_ENTITY)
def assert_state(actual: State | None, expected: State | None) -> None:
"""Assert that the two states are equal."""
@@ -108,118 +44,29 @@ def assert_state(actual: State | None, expected: State | None) -> None:
assert actual.attributes == expected.attributes
async def test_init_success(
@pytest.fixture
def setup_config_entry(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> MockConfigEntry:
"""Fixture to initialize the config entry."""
config_entry.add_to_hass(hass)
async def test_unload_entry(
hass: HomeAssistant,
mock_code_flow: Mock,
mock_exchange: Mock,
mock_notification: Mock,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_calendars_yaml: None,
component_setup: ComponentSetup,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test successful creds setup."""
mock_calendars_list({"items": [test_api_calendar]})
assert await component_setup()
"""Test load and unload of a ConfigEntry."""
await component_setup()
# Run one tick to invoke the credential exchange check
now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
entry = entries[0]
assert entry.state is ConfigEntryState.LOADED
state = hass.states.get(TEST_YAML_ENTITY)
assert state
assert state.name == TEST_YAML_ENTITY_NAME
assert state.state == STATE_OFF
mock_notification.assert_called()
assert "We are all setup now" in mock_notification.call_args[0][1]
async def test_code_error(
hass: HomeAssistant,
mock_code_flow: Mock,
component_setup: ComponentSetup,
mock_notification: Mock,
) -> None:
"""Test loading the integration with no existing credentials."""
with patch(
"oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes",
side_effect=OAuth2DeviceCodeError("Test Failure"),
):
assert await component_setup()
assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_called()
assert "Error: Test Failure" in mock_notification.call_args[0][1]
@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)])
async def test_expired_after_exchange(
hass: HomeAssistant,
mock_code_flow: Mock,
component_setup: ComponentSetup,
mock_notification: Mock,
) -> None:
"""Test loading the integration with no existing credentials."""
assert await component_setup()
now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_called()
assert (
"Authentication code expired, please restart Home-Assistant and try again"
in mock_notification.call_args[0][1]
)
async def test_exchange_error(
hass: HomeAssistant,
mock_code_flow: Mock,
component_setup: ComponentSetup,
mock_notification: Mock,
) -> None:
"""Test an error while exchanging the code for credentials."""
with patch(
"oauth2client.client.OAuth2WebServerFlow.step2_exchange",
side_effect=FlowExchangeError(),
):
assert await component_setup()
now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_called()
assert "In order to authorize Home-Assistant" in mock_notification.call_args[0][1]
async def test_existing_token(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_yaml: None,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_notification: Mock,
) -> None:
"""Test setup with an existing token file."""
mock_calendars_list({"items": [test_api_calendar]})
assert await component_setup()
state = hass.states.get(TEST_YAML_ENTITY)
assert state
assert state.name == TEST_YAML_ENTITY_NAME
assert state.state == STATE_OFF
mock_notification.assert_not_called()
assert await hass.config_entries.async_unload(entry.entry_id)
assert entry.state == ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
@@ -228,80 +75,61 @@ async def test_existing_token(
async def test_existing_token_missing_scope(
hass: HomeAssistant,
token_scopes: list[str],
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_yaml: None,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_notification: Mock,
mock_code_flow: Mock,
mock_exchange: Mock,
config_entry: MockConfigEntry,
) -> None:
"""Test setup where existing token does not have sufficient scopes."""
mock_calendars_list({"items": [test_api_calendar]})
config_entry.add_to_hass(hass)
assert await component_setup()
# Run one tick to invoke the credential exchange check
now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
assert len(mock_exchange.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.SETUP_ERROR
state = hass.states.get(TEST_YAML_ENTITY)
assert state
assert state.name == TEST_YAML_ENTITY_NAME
assert state.state == STATE_OFF
# No notifications on success
mock_notification.assert_called()
assert "We are all setup now" in mock_notification.call_args[0][1]
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
@pytest.mark.parametrize("calendars_config", [[{"cal_id": "invalid-schema"}]])
async def test_calendar_yaml_missing_required_fields(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
calendars_config: list[dict[str, Any]],
mock_calendars_yaml: None,
mock_notification: Mock,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test setup with a missing schema fields, ignores the error and continues."""
assert await component_setup()
assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_not_called()
@pytest.mark.parametrize("calendars_config", [[{"missing-cal_id": "invalid-schema"}]])
async def test_invalid_calendar_yaml(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
calendars_config: list[dict[str, Any]],
mock_calendars_yaml: None,
mock_notification: Mock,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test setup with missing entity id fields fails to setup the integration."""
"""Test setup with missing entity id fields fails to setup the config entry."""
# Integration fails to setup
assert not await component_setup()
assert await component_setup()
# XXX No config entries
assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_not_called()
async def test_calendar_yaml_error(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_notification: Mock,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test setup with yaml file not found."""
mock_calendars_list({"items": [test_api_calendar]})
with patch("homeassistant.components.google.open", side_effect=FileNotFoundError()):
@@ -344,12 +172,12 @@ async def test_calendar_yaml_error(
)
async def test_track_new(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_calendars_yaml: None,
expected_state: State,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test behavior of configuration.yaml settings for tracking new calendars not in the config."""
@@ -363,11 +191,11 @@ async def test_track_new(
@pytest.mark.parametrize("calendars_config", [[]])
async def test_found_calendar_from_api(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_yaml: None,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
setup_config_entry: MockConfigEntry,
) -> None:
"""Test finding a calendar from the API."""
@@ -402,13 +230,13 @@ async def test_found_calendar_from_api(
)
async def test_calendar_config_track_new(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_yaml: None,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
calendars_config_track: bool,
expected_state: State,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test calendar config that overrides whether or not a calendar is tracked."""
@@ -421,11 +249,11 @@ async def test_calendar_config_track_new(
async def test_add_event(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_insert_event: Mock,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test service call that adds an event."""
@@ -471,7 +299,6 @@ async def test_add_event(
)
async def test_add_event_date_in_x(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
@@ -479,6 +306,7 @@ async def test_add_event_date_in_x(
date_fields: dict[str, Any],
start_timedelta: datetime.timedelta,
end_timedelta: datetime.timedelta,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test service call that adds an event with various time ranges."""
@@ -514,10 +342,10 @@ async def test_add_event_date_in_x(
async def test_add_event_date(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
mock_insert_event: Mock,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test service call that sets a date range."""
@@ -554,11 +382,11 @@ async def test_add_event_date(
async def test_add_event_date_time(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_insert_event: Mock,
setup_config_entry: MockConfigEntry,
) -> None:
"""Test service call that adds an event with a date time range."""
@@ -601,10 +429,10 @@ async def test_add_event_date_time(
async def test_scan_calendars(
hass: HomeAssistant,
mock_token_read: None,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
setup_config_entry: MockConfigEntry,
) -> None:
"""Test finding a calendar from the API."""