Improve google calendar test coverage to 97% (#65223)
* Improve google calendar test coverage to 97% * Remove commented out code. * Remove unnecessary (flaky) checks for token file persistence * Remove mock code assertions * Add debug logging to google calendar integration * Increase alarm time to polling code to reduce flakes * Setup every test in their own configuration directory * Mock out filesystem calls to avoid disk dependencies Update scope checking code to use Storage object rather than text file matching * Update tests to check entity states when integration is loaded * Mock out google service in multiple locations * Update homeassistant/components/google/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/google/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/65352/head
parent
5289935ac1
commit
88ed2f3b3e
|
@ -407,7 +407,6 @@ omit =
|
||||||
homeassistant/components/goodwe/number.py
|
homeassistant/components/goodwe/number.py
|
||||||
homeassistant/components/goodwe/select.py
|
homeassistant/components/goodwe/select.py
|
||||||
homeassistant/components/goodwe/sensor.py
|
homeassistant/components/goodwe/sensor.py
|
||||||
homeassistant/components/google/__init__.py
|
|
||||||
homeassistant/components/google_cloud/tts.py
|
homeassistant/components/google_cloud/tts.py
|
||||||
homeassistant/components/google_maps/device_tracker.py
|
homeassistant/components/google_maps/device_tracker.py
|
||||||
homeassistant/components/google_pubsub/__init__.py
|
homeassistant/components/google_pubsub/__init__.py
|
||||||
|
|
|
@ -194,6 +194,7 @@ def do_authentication(hass, hass_config, config):
|
||||||
|
|
||||||
def step2_exchange(now):
|
def step2_exchange(now):
|
||||||
"""Keep trying to validate the user_code until it expires."""
|
"""Keep trying to validate the user_code until it expires."""
|
||||||
|
_LOGGER.debug("Attempting to validate user code")
|
||||||
|
|
||||||
# For some reason, oauth.step1_get_device_and_user_codes() returns a datetime
|
# For some reason, oauth.step1_get_device_and_user_codes() returns a datetime
|
||||||
# object without tzinfo. For the comparison below to work, it needs one.
|
# object without tzinfo. For the comparison below to work, it needs one.
|
||||||
|
@ -208,6 +209,7 @@ def do_authentication(hass, hass_config, config):
|
||||||
notification_id=NOTIFICATION_ID,
|
notification_id=NOTIFICATION_ID,
|
||||||
)
|
)
|
||||||
listener()
|
listener()
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
credentials = oauth.step2_exchange(device_flow_info=dev_flow)
|
credentials = oauth.step2_exchange(device_flow_info=dev_flow)
|
||||||
|
@ -247,9 +249,11 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
||||||
token_file = hass.config.path(TOKEN_FILE)
|
token_file = hass.config.path(TOKEN_FILE)
|
||||||
if not os.path.isfile(token_file):
|
if not os.path.isfile(token_file):
|
||||||
|
_LOGGER.debug("Token file does not exist, authenticating for first time")
|
||||||
do_authentication(hass, config, conf)
|
do_authentication(hass, config, conf)
|
||||||
else:
|
else:
|
||||||
if not check_correct_scopes(token_file, conf):
|
if not check_correct_scopes(hass, token_file, conf):
|
||||||
|
_LOGGER.debug("Existing scopes are not sufficient, re-authenticating")
|
||||||
do_authentication(hass, config, conf)
|
do_authentication(hass, config, conf)
|
||||||
else:
|
else:
|
||||||
do_setup(hass, config, conf)
|
do_setup(hass, config, conf)
|
||||||
|
@ -257,17 +261,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_correct_scopes(token_file, config):
|
def check_correct_scopes(hass, token_file, config):
|
||||||
"""Check for the correct scopes in file."""
|
"""Check for the correct scopes in file."""
|
||||||
with open(token_file, encoding="utf8") as tokenfile:
|
creds = Storage(token_file).get()
|
||||||
contents = tokenfile.read()
|
if not creds or not creds.scopes:
|
||||||
|
return False
|
||||||
# Check for quoted scope as our scopes can be subsets of other scopes
|
target_scope = config[CONF_CALENDAR_ACCESS].scope
|
||||||
target_scope = f'"{config.get(CONF_CALENDAR_ACCESS).scope}"'
|
return target_scope in creds.scopes
|
||||||
if target_scope not in contents:
|
|
||||||
_LOGGER.warning("Please re-authenticate with Google")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def setup_services(
|
def setup_services(
|
||||||
|
@ -364,6 +364,7 @@ def setup_services(
|
||||||
|
|
||||||
def do_setup(hass, hass_config, config):
|
def do_setup(hass, hass_config, config):
|
||||||
"""Run the setup after we have everything configured."""
|
"""Run the setup after we have everything configured."""
|
||||||
|
_LOGGER.debug("Setting up integration")
|
||||||
# Load calendars the user has configured
|
# Load calendars the user has configured
|
||||||
hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES))
|
hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES))
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
"""Test configuration and mocks for the google integration."""
|
"""Test configuration and mocks for the google integration."""
|
||||||
from unittest.mock import patch
|
from collections.abc import Callable
|
||||||
|
from typing import Any, Generator, TypeVar
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.google import GoogleCalendarService
|
||||||
|
|
||||||
|
ApiResult = Callable[[dict[str, Any]], None]
|
||||||
|
T = TypeVar("T")
|
||||||
|
YieldFixture = Generator[T, None, None]
|
||||||
|
|
||||||
|
|
||||||
|
CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com"
|
||||||
TEST_CALENDAR = {
|
TEST_CALENDAR = {
|
||||||
"id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com",
|
"id": CALENDAR_ID,
|
||||||
"etag": '"3584134138943410"',
|
"etag": '"3584134138943410"',
|
||||||
"timeZone": "UTC",
|
"timeZone": "UTC",
|
||||||
"accessRole": "reader",
|
"accessRole": "reader",
|
||||||
|
@ -34,3 +44,45 @@ def mock_next_event():
|
||||||
)
|
)
|
||||||
with patch_google_cal as google_cal_data:
|
with patch_google_cal as google_cal_data:
|
||||||
yield google_cal_data
|
yield google_cal_data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_events_list(
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
) -> Callable[[dict[str, Any]], None]:
|
||||||
|
"""Fixture to construct a fake event list API response."""
|
||||||
|
|
||||||
|
def _put_result(response: dict[str, Any]) -> None:
|
||||||
|
google_service.return_value.get.return_value.events.return_value.list.return_value.execute.return_value = (
|
||||||
|
response
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
return _put_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_calendars_list(
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
) -> ApiResult:
|
||||||
|
"""Fixture to construct a fake calendar list API response."""
|
||||||
|
|
||||||
|
def _put_result(response: dict[str, Any]) -> None:
|
||||||
|
google_service.return_value.get.return_value.calendarList.return_value.list.return_value.execute.return_value = (
|
||||||
|
response
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
return _put_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_insert_event(
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
) -> Mock:
|
||||||
|
"""Fixture to create a mock to capture new events added to the API."""
|
||||||
|
insert_mock = Mock()
|
||||||
|
google_service.return_value.get.return_value.events.return_value.insert = (
|
||||||
|
insert_mock
|
||||||
|
)
|
||||||
|
return insert_mock
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
import copy
|
import copy
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -22,7 +21,6 @@ from homeassistant.components.google import (
|
||||||
CONF_TRACK,
|
CONF_TRACK,
|
||||||
DEVICE_SCHEMA,
|
DEVICE_SCHEMA,
|
||||||
SERVICE_SCAN_CALENDARS,
|
SERVICE_SCAN_CALENDARS,
|
||||||
GoogleCalendarService,
|
|
||||||
do_setup,
|
do_setup,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
|
@ -358,21 +356,6 @@ async def test_http_event_api_failure(hass, hass_client, google_service):
|
||||||
assert events == []
|
assert events == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_events_list(
|
|
||||||
google_service: GoogleCalendarService,
|
|
||||||
) -> Callable[[dict[str, Any]], None]:
|
|
||||||
"""Fixture to construct a fake event list API response."""
|
|
||||||
|
|
||||||
def _put_result(response: dict[str, Any]) -> None:
|
|
||||||
google_service.return_value.get.return_value.events.return_value.list.return_value.execute.return_value = (
|
|
||||||
response
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
return _put_result
|
|
||||||
|
|
||||||
|
|
||||||
async def test_http_api_event(hass, hass_client, google_service, mock_events_list):
|
async def test_http_api_event(hass, hass_client, google_service, mock_events_list):
|
||||||
"""Test querying the API and fetching events from the server."""
|
"""Test querying the API and fetching events from the server."""
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
|
|
|
@ -1,67 +1,497 @@
|
||||||
"""The tests for the Google Calendar component."""
|
"""The tests for the Google Calendar component."""
|
||||||
from unittest.mock import patch
|
from collections.abc import Awaitable, Callable
|
||||||
|
import datetime
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import Mock, call, mock_open, patch
|
||||||
|
|
||||||
|
from oauth2client.client import (
|
||||||
|
FlowExchangeError,
|
||||||
|
OAuth2Credentials,
|
||||||
|
OAuth2DeviceCodeError,
|
||||||
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
import homeassistant.components.google as google
|
from homeassistant.components.google import (
|
||||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
DOMAIN,
|
||||||
|
SERVICE_ADD_EVENT,
|
||||||
|
GoogleCalendarService,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_OFF
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
from .conftest import CALENDAR_ID, ApiResult, YieldFixture
|
||||||
|
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
# Typing helpers
|
||||||
|
ComponentSetup = Callable[[], Awaitable[bool]]
|
||||||
|
HassApi = Callable[[], Awaitable[dict[str, Any]]]
|
||||||
|
|
||||||
|
CODE_CHECK_INTERVAL = 1
|
||||||
|
CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="google_setup")
|
@pytest.fixture
|
||||||
def mock_google_setup(hass):
|
async def code_expiration_delta() -> datetime.timedelta:
|
||||||
"""Mock the google set up functions."""
|
"""Fixture for code expiration time, defaulting to the future."""
|
||||||
p_auth = patch(
|
return datetime.timedelta(minutes=3)
|
||||||
"homeassistant.components.google.do_authentication", side_effect=google.do_setup
|
|
||||||
|
|
||||||
|
@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 token_scopes() -> list[str]:
|
||||||
|
"""Fixture for scopes used during test."""
|
||||||
|
return ["https://www.googleapis.com/auth/calendar"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def creds(token_scopes: list[str]) -> OAuth2Credentials:
|
||||||
|
"""Fixture that defines creds used in the test."""
|
||||||
|
token_expiry = utcnow() + datetime.timedelta(days=7)
|
||||||
|
return OAuth2Credentials(
|
||||||
|
access_token="ACCESS_TOKEN",
|
||||||
|
client_id="client-id",
|
||||||
|
client_secret="client-secret",
|
||||||
|
refresh_token="REFRESH_TOKEN",
|
||||||
|
token_expiry=token_expiry,
|
||||||
|
token_uri="http://example.com",
|
||||||
|
user_agent="n/a",
|
||||||
|
scopes=token_scopes,
|
||||||
)
|
)
|
||||||
p_service = patch("homeassistant.components.google.GoogleCalendarService.get")
|
|
||||||
p_discovery = patch("homeassistant.components.google.discovery.load_platform")
|
|
||||||
p_load = patch("homeassistant.components.google.load_config", return_value={})
|
|
||||||
p_save = patch("homeassistant.components.google.update_config")
|
|
||||||
|
|
||||||
with p_auth, p_load, p_service, p_discovery, p_save:
|
|
||||||
|
@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(autouse=True)
|
||||||
|
async def mock_token_write(hass: HomeAssistant) -> None:
|
||||||
|
"""Fixture to avoid writing token files to disk."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.google.os.path.isfile", return_value=True
|
||||||
|
), patch("homeassistant.components.google.Storage.put"):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_component(hass, google_setup):
|
@pytest.fixture
|
||||||
"""Test setup component."""
|
async def mock_token_read(
|
||||||
config = {"google": {CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret"}}
|
hass: HomeAssistant,
|
||||||
|
creds: OAuth2Credentials,
|
||||||
assert await async_setup_component(hass, "google", config)
|
) -> None:
|
||||||
|
"""Fixture to populate an existing token file."""
|
||||||
|
with patch("homeassistant.components.google.Storage.get", return_value=creds):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
async def test_get_calendar_info(hass, test_calendar):
|
@pytest.fixture
|
||||||
"""Test getting the calendar info."""
|
async def calendars_config() -> list[dict[str, Any]]:
|
||||||
calendar_info = await hass.async_add_executor_job(
|
"""Fixture for tests to override default calendar configuration."""
|
||||||
google.get_calendar_info, hass, test_calendar
|
return [
|
||||||
)
|
{
|
||||||
assert calendar_info == {
|
"cal_id": CALENDAR_ID,
|
||||||
"cal_id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com",
|
"entities": [
|
||||||
"entities": [
|
{
|
||||||
{
|
"device_id": "backyard_light",
|
||||||
"device_id": "we_are_we_are_a_test_calendar",
|
"name": "Backyard Light",
|
||||||
"name": "We are, we are, a... Test Calendar",
|
"search": "#Backyard",
|
||||||
"track": True,
|
"track": True,
|
||||||
"ignore_availability": True,
|
}
|
||||||
}
|
],
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_found_calendar(hass, google_setup, mock_next_event, test_calendar):
|
|
||||||
"""Test when a calendar is found."""
|
|
||||||
config = {
|
|
||||||
"google": {
|
|
||||||
CONF_CLIENT_ID: "id",
|
|
||||||
CONF_CLIENT_SECRET: "secret",
|
|
||||||
"track_new_calendar": True,
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
assert await async_setup_component(hass, "google", config)
|
|
||||||
assert hass.data[google.DATA_INDEX] == {}
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def mock_calendars_yaml(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
calendars_config: list[dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""Fixture that prepares the calendars.yaml file."""
|
||||||
|
mocked_open_function = mock_open(read_data=yaml.dump(calendars_config))
|
||||||
|
with patch("homeassistant.components.google.open", mocked_open_function):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def mock_notification() -> YieldFixture[Mock]:
|
||||||
|
"""Fixture for capturing persistent notifications."""
|
||||||
|
with patch("homeassistant.components.persistent_notification.create") as mock:
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def config() -> dict[str, Any]:
|
||||||
|
"""Fixture for overriding component config."""
|
||||||
|
return {DOMAIN: {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-ecret"}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def component_setup(
|
||||||
|
hass: HomeAssistant, config: dict[str, Any]
|
||||||
|
) -> ComponentSetup:
|
||||||
|
"""Fixture for setting up the integration."""
|
||||||
|
|
||||||
|
async def _setup_func() -> bool:
|
||||||
|
result = await async_setup_component(hass, DOMAIN, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
return result
|
||||||
|
|
||||||
|
return _setup_func
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def google_service() -> YieldFixture[GoogleCalendarService]:
|
||||||
|
"""Fixture to capture service calls."""
|
||||||
|
with patch("homeassistant.components.google.GoogleCalendarService") as mock, patch(
|
||||||
|
"homeassistant.components.google.calendar.GoogleCalendarService", 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("calendar.backyard_light")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_init_success(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
mock_code_flow: Mock,
|
||||||
|
mock_exchange: Mock,
|
||||||
|
mock_notification: Mock,
|
||||||
|
mock_calendars_yaml: None,
|
||||||
|
component_setup: ComponentSetup,
|
||||||
|
) -> None:
|
||||||
|
"""Test successful creds setup."""
|
||||||
|
assert await component_setup()
|
||||||
|
|
||||||
|
# Run one tick to invoke the credential exchange check
|
||||||
|
now = utcnow()
|
||||||
|
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
|
||||||
|
|
||||||
|
state = hass.states.get("calendar.backyard_light")
|
||||||
|
assert state
|
||||||
|
assert state.name == "Backyard Light"
|
||||||
|
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("calendar.backyard_light")
|
||||||
|
|
||||||
|
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("calendar.backyard_light")
|
||||||
|
|
||||||
|
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("calendar.backyard_light")
|
||||||
|
|
||||||
|
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,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
mock_calendars_yaml: None,
|
||||||
|
mock_notification: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup with an existing token file."""
|
||||||
|
assert await component_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("calendar.backyard_light")
|
||||||
|
assert state
|
||||||
|
assert state.name == "Backyard Light"
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
mock_notification.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"token_scopes", ["https://www.googleapis.com/auth/calendar.readonly"]
|
||||||
|
)
|
||||||
|
async def test_existing_token_missing_scope(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
token_scopes: list[str],
|
||||||
|
mock_token_read: None,
|
||||||
|
component_setup: ComponentSetup,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
mock_calendars_yaml: None,
|
||||||
|
mock_notification: Mock,
|
||||||
|
mock_code_flow: Mock,
|
||||||
|
mock_exchange: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup where existing token does not have sufficient scopes."""
|
||||||
|
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
|
||||||
|
|
||||||
|
state = hass.states.get("calendar.backyard_light")
|
||||||
|
assert state
|
||||||
|
assert state.name == "Backyard Light"
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
@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,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
calendars_config: list[dict[str, Any]],
|
||||||
|
mock_calendars_yaml: None,
|
||||||
|
mock_notification: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup with a missing schema fields, ignores the error and continues."""
|
||||||
|
assert await component_setup()
|
||||||
|
|
||||||
|
assert not hass.states.get("calendar.backyard_light")
|
||||||
|
|
||||||
|
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,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
calendars_config: list[dict[str, Any]],
|
||||||
|
mock_calendars_yaml: None,
|
||||||
|
mock_notification: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup with missing entity id fields fails to setup the integration."""
|
||||||
|
|
||||||
|
# Integration fails to setup
|
||||||
|
assert not await component_setup()
|
||||||
|
|
||||||
|
assert not hass.states.get("calendar.backyard_light")
|
||||||
|
|
||||||
|
mock_notification.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_found_calendar_from_api(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_token_read: None,
|
||||||
|
component_setup: ComponentSetup,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
mock_calendars_list: ApiResult,
|
||||||
|
test_calendar: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test finding a calendar from the API."""
|
||||||
|
|
||||||
|
mock_calendars_list({"items": [test_calendar]})
|
||||||
|
|
||||||
|
mocked_open_function = mock_open(read_data=yaml.dump([]))
|
||||||
|
with patch("homeassistant.components.google.open", mocked_open_function):
|
||||||
|
assert await component_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("calendar.we_are_we_are_a_test_calendar")
|
||||||
|
assert state
|
||||||
|
assert state.name == "We are, we are, a... Test Calendar"
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_add_event(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_token_read: None,
|
||||||
|
component_setup: ComponentSetup,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
mock_calendars_list: ApiResult,
|
||||||
|
test_calendar: dict[str, Any],
|
||||||
|
mock_insert_event: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test service call that adds an event."""
|
||||||
|
|
||||||
|
assert await component_setup()
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"google", google.SERVICE_FOUND_CALENDARS, test_calendar, blocking=True
|
DOMAIN,
|
||||||
|
SERVICE_ADD_EVENT,
|
||||||
|
{
|
||||||
|
"calendar_id": CALENDAR_ID,
|
||||||
|
"summary": "Summary",
|
||||||
|
"description": "Description",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_insert_event.assert_called()
|
||||||
|
assert mock_insert_event.mock_calls[0] == call(
|
||||||
|
calendarId=CALENDAR_ID,
|
||||||
|
body={
|
||||||
|
"summary": "Summary",
|
||||||
|
"description": "Description",
|
||||||
|
"start": {},
|
||||||
|
"end": {},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert hass.data[google.DATA_INDEX].get(test_calendar["id"]) is not None
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"date_fields,start_timedelta,end_timedelta",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"in": {"days": 3}},
|
||||||
|
datetime.timedelta(days=3),
|
||||||
|
datetime.timedelta(days=4),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"in": {"weeks": 1}},
|
||||||
|
datetime.timedelta(days=7),
|
||||||
|
datetime.timedelta(days=8),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"start_date": datetime.date.today().isoformat(),
|
||||||
|
"end_date": (
|
||||||
|
datetime.date.today() + datetime.timedelta(days=2)
|
||||||
|
).isoformat(),
|
||||||
|
},
|
||||||
|
datetime.timedelta(days=0),
|
||||||
|
datetime.timedelta(days=2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=["in_days", "in_weeks", "explit_date"],
|
||||||
|
)
|
||||||
|
async def test_add_event_date_ranges(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_token_read: None,
|
||||||
|
calendars_config: list[dict[str, Any]],
|
||||||
|
component_setup: ComponentSetup,
|
||||||
|
google_service: GoogleCalendarService,
|
||||||
|
mock_calendars_list: ApiResult,
|
||||||
|
test_calendar: dict[str, Any],
|
||||||
|
mock_insert_event: Mock,
|
||||||
|
date_fields: dict[str, Any],
|
||||||
|
start_timedelta: datetime.timedelta,
|
||||||
|
end_timedelta: datetime.timedelta,
|
||||||
|
) -> None:
|
||||||
|
"""Test service call that adds an event with various time ranges."""
|
||||||
|
|
||||||
|
assert await component_setup()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ADD_EVENT,
|
||||||
|
{
|
||||||
|
"calendar_id": CALENDAR_ID,
|
||||||
|
"summary": "Summary",
|
||||||
|
"description": "Description",
|
||||||
|
**date_fields,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_insert_event.assert_called()
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
start_date = now + start_timedelta
|
||||||
|
end_date = now + end_timedelta
|
||||||
|
|
||||||
|
assert mock_insert_event.mock_calls[0] == call(
|
||||||
|
calendarId=CALENDAR_ID,
|
||||||
|
body={
|
||||||
|
"summary": "Summary",
|
||||||
|
"description": "Description",
|
||||||
|
"start": {"date": start_date.date().isoformat()},
|
||||||
|
"end": {"date": end_date.date().isoformat()},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue