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:
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user