2019-05-05 16:38:55 +00:00
|
|
|
"""Test configuration and mocks for the google integration."""
|
2022-02-26 23:17:02 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-03-04 07:12:24 +00:00
|
|
|
from collections.abc import Awaitable, Callable
|
2022-02-26 23:17:02 +00:00
|
|
|
import datetime
|
2022-06-21 13:42:41 +00:00
|
|
|
import http
|
2022-02-01 02:14:49 +00:00
|
|
|
from typing import Any, Generator, TypeVar
|
2022-05-25 06:59:27 +00:00
|
|
|
from unittest.mock import Mock, mock_open, patch
|
2019-05-05 16:38:55 +00:00
|
|
|
|
2022-05-21 18:22:27 +00:00
|
|
|
from aiohttp.client_exceptions import ClientError
|
2022-04-21 03:18:24 +00:00
|
|
|
from gcal_sync.auth import API_BASE_URL
|
2022-02-26 23:17:02 +00:00
|
|
|
from oauth2client.client import Credentials, OAuth2Credentials
|
2021-01-01 21:31:56 +00:00
|
|
|
import pytest
|
2022-03-04 07:12:24 +00:00
|
|
|
import yaml
|
2020-05-03 18:27:19 +00:00
|
|
|
|
2022-03-04 07:12:24 +00:00
|
|
|
from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN
|
|
|
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
2022-02-26 23:17:02 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2022-03-04 07:12:24 +00:00
|
|
|
from homeassistant.setup import async_setup_component
|
2022-02-01 02:14:49 +00:00
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
from tests.common import MockConfigEntry
|
2022-04-21 03:18:24 +00:00
|
|
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
2022-03-15 06:51:02 +00:00
|
|
|
|
2022-02-01 02:14:49 +00:00
|
|
|
ApiResult = Callable[[dict[str, Any]], None]
|
2022-03-04 07:12:24 +00:00
|
|
|
ComponentSetup = Callable[[], Awaitable[bool]]
|
2022-03-17 18:11:14 +00:00
|
|
|
_T = TypeVar("_T")
|
|
|
|
YieldFixture = Generator[_T, None, None]
|
2022-02-01 02:14:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com"
|
2022-06-21 13:42:41 +00:00
|
|
|
EMAIL_ADDRESS = "user@gmail.com"
|
2022-03-04 07:12:24 +00:00
|
|
|
|
|
|
|
# Entities can either be created based on data directly from the API, or from
|
|
|
|
# the yaml config that overrides the entity name and other settings. A test
|
|
|
|
# can use a fixture to exercise either case.
|
|
|
|
TEST_API_ENTITY = "calendar.we_are_we_are_a_test_calendar"
|
|
|
|
TEST_API_ENTITY_NAME = "We are, we are, a... Test Calendar"
|
|
|
|
# Name of the entity when using yaml configuration overrides
|
|
|
|
TEST_YAML_ENTITY = "calendar.backyard_light"
|
|
|
|
TEST_YAML_ENTITY_NAME = "Backyard Light"
|
|
|
|
|
|
|
|
# A calendar object returned from the API
|
|
|
|
TEST_API_CALENDAR = {
|
2022-02-01 02:14:49 +00:00
|
|
|
"id": CALENDAR_ID,
|
2019-07-31 19:25:30 +00:00
|
|
|
"etag": '"3584134138943410"',
|
|
|
|
"timeZone": "UTC",
|
|
|
|
"accessRole": "reader",
|
|
|
|
"foregroundColor": "#000000",
|
|
|
|
"selected": True,
|
|
|
|
"kind": "calendar#calendarListEntry",
|
|
|
|
"backgroundColor": "#16a765",
|
|
|
|
"description": "Test Calendar",
|
|
|
|
"summary": "We are, we are, a... Test Calendar",
|
|
|
|
"colorId": "8",
|
|
|
|
"defaultReminders": [],
|
2019-05-05 16:38:55 +00:00
|
|
|
}
|
|
|
|
|
2022-06-21 13:42:41 +00:00
|
|
|
CLIENT_ID = "client-id"
|
|
|
|
CLIENT_SECRET = "client-secret"
|
|
|
|
|
2019-05-05 16:38:55 +00:00
|
|
|
|
|
|
|
@pytest.fixture
|
2022-03-04 07:12:24 +00:00
|
|
|
def test_api_calendar():
|
|
|
|
"""Return a test calendar object used in API responses."""
|
|
|
|
return TEST_API_CALENDAR
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def calendars_config_track() -> bool:
|
|
|
|
"""Fixture that determines the 'track' setting in yaml config."""
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def calendars_config_ignore_availability() -> bool:
|
|
|
|
"""Fixture that determines the 'ignore_availability' setting in yaml config."""
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def calendars_config_entity(
|
|
|
|
calendars_config_track: bool, calendars_config_ignore_availability: bool | None
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
"""Fixture that creates an entity within the yaml configuration."""
|
|
|
|
entity = {
|
|
|
|
"device_id": "backyard_light",
|
|
|
|
"name": "Backyard Light",
|
|
|
|
"search": "#Backyard",
|
|
|
|
"track": calendars_config_track,
|
|
|
|
}
|
|
|
|
if calendars_config_ignore_availability is not None:
|
|
|
|
entity["ignore_availability"] = calendars_config_ignore_availability
|
|
|
|
return entity
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
|
"""Fixture that specifies the calendar yaml configuration."""
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
"cal_id": CALENDAR_ID,
|
|
|
|
"entities": [calendars_config_entity],
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2022-06-17 07:04:41 +00:00
|
|
|
@pytest.fixture
|
2022-04-09 06:33:24 +00:00
|
|
|
def mock_calendars_yaml(
|
2022-03-04 07:12:24 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
calendars_config: list[dict[str, Any]],
|
2022-05-25 06:59:27 +00:00
|
|
|
) -> Generator[Mock, None, None]:
|
2022-03-04 07:12:24 +00:00
|
|
|
"""Fixture that prepares the google_calendars.yaml mocks."""
|
|
|
|
mocked_open_function = mock_open(read_data=yaml.dump(calendars_config))
|
|
|
|
with patch("homeassistant.components.google.open", mocked_open_function):
|
2022-05-25 06:59:27 +00:00
|
|
|
yield mocked_open_function
|
2019-05-05 16:38:55 +00:00
|
|
|
|
|
|
|
|
2022-02-26 23:17:02 +00:00
|
|
|
class FakeStorage:
|
|
|
|
"""A fake storage object for persiting creds."""
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
"""Initialize FakeStorage."""
|
|
|
|
self._creds: Credentials | None = None
|
|
|
|
|
|
|
|
def get(self) -> Credentials | None:
|
|
|
|
"""Get credentials from storage."""
|
|
|
|
return self._creds
|
|
|
|
|
|
|
|
def put(self, creds: Credentials) -> None:
|
|
|
|
"""Put credentials in storage."""
|
|
|
|
self._creds = creds
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-04-09 06:33:24 +00:00
|
|
|
def token_scopes() -> list[str]:
|
2022-02-26 23:17:02 +00:00
|
|
|
"""Fixture for scopes used during test."""
|
|
|
|
return ["https://www.googleapis.com/auth/calendar"]
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-04-09 03:27:58 +00:00
|
|
|
def token_expiry() -> datetime.datetime:
|
|
|
|
"""Expiration time for credentials used in the test."""
|
2022-06-03 23:33:12 +00:00
|
|
|
# OAuth library returns an offset-naive timestamp
|
|
|
|
return datetime.datetime.fromtimestamp(
|
|
|
|
datetime.datetime.utcnow().timestamp()
|
|
|
|
) + datetime.timedelta(hours=1)
|
2022-04-09 03:27:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def creds(
|
|
|
|
token_scopes: list[str], token_expiry: datetime.datetime
|
|
|
|
) -> OAuth2Credentials:
|
2022-02-26 23:17:02 +00:00
|
|
|
"""Fixture that defines creds used in the test."""
|
|
|
|
return OAuth2Credentials(
|
|
|
|
access_token="ACCESS_TOKEN",
|
2022-06-21 13:42:41 +00:00
|
|
|
client_id=CLIENT_ID,
|
|
|
|
client_secret=CLIENT_SECRET,
|
2022-02-26 23:17:02 +00:00
|
|
|
refresh_token="REFRESH_TOKEN",
|
|
|
|
token_expiry=token_expiry,
|
|
|
|
token_uri="http://example.com",
|
|
|
|
user_agent="n/a",
|
|
|
|
scopes=token_scopes,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
2022-04-09 06:33:24 +00:00
|
|
|
def storage() -> YieldFixture[FakeStorage]:
|
2022-02-26 23:17:02 +00:00
|
|
|
"""Fixture to populate an existing token file for read on startup."""
|
|
|
|
storage = FakeStorage()
|
|
|
|
with patch("homeassistant.components.google.Storage", return_value=storage):
|
|
|
|
yield storage
|
|
|
|
|
|
|
|
|
2022-03-15 06:51:02 +00:00
|
|
|
@pytest.fixture
|
2022-04-09 03:27:58 +00:00
|
|
|
def config_entry_token_expiry(token_expiry: datetime.datetime) -> float:
|
|
|
|
"""Fixture for token expiration value stored in the config entry."""
|
|
|
|
return token_expiry.timestamp()
|
|
|
|
|
|
|
|
|
2022-05-25 06:59:27 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def config_entry_options() -> dict[str, Any] | None:
|
|
|
|
"""Fixture to set initial config entry options."""
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2022-06-21 13:42:41 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def config_entry_unique_id() -> str:
|
|
|
|
"""Fixture that returns the default config entry unique id."""
|
|
|
|
return EMAIL_ADDRESS
|
|
|
|
|
|
|
|
|
2022-04-09 03:27:58 +00:00
|
|
|
@pytest.fixture
|
2022-04-09 06:33:24 +00:00
|
|
|
def config_entry(
|
2022-06-21 13:42:41 +00:00
|
|
|
config_entry_unique_id: str,
|
2022-05-14 17:27:47 +00:00
|
|
|
token_scopes: list[str],
|
|
|
|
config_entry_token_expiry: float,
|
2022-05-25 06:59:27 +00:00
|
|
|
config_entry_options: dict[str, Any] | None,
|
2022-04-09 03:27:58 +00:00
|
|
|
) -> MockConfigEntry:
|
2022-03-15 06:51:02 +00:00
|
|
|
"""Fixture to create a config entry for the integration."""
|
|
|
|
return MockConfigEntry(
|
|
|
|
domain=DOMAIN,
|
2022-06-21 13:42:41 +00:00
|
|
|
unique_id=config_entry_unique_id,
|
2022-03-15 06:51:02 +00:00
|
|
|
data={
|
|
|
|
"auth_implementation": "device_auth",
|
|
|
|
"token": {
|
|
|
|
"access_token": "ACCESS_TOKEN",
|
|
|
|
"refresh_token": "REFRESH_TOKEN",
|
|
|
|
"scope": " ".join(token_scopes),
|
|
|
|
"token_type": "Bearer",
|
2022-04-09 03:27:58 +00:00
|
|
|
"expires_at": config_entry_token_expiry,
|
2022-03-15 06:51:02 +00:00
|
|
|
},
|
|
|
|
},
|
2022-05-25 06:59:27 +00:00
|
|
|
options=config_entry_options,
|
2022-03-15 06:51:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-02-26 23:17:02 +00:00
|
|
|
@pytest.fixture
|
2022-04-09 06:33:24 +00:00
|
|
|
def mock_token_read(
|
2022-02-26 23:17:02 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
creds: OAuth2Credentials,
|
|
|
|
storage: FakeStorage,
|
|
|
|
) -> None:
|
|
|
|
"""Fixture to populate an existing token file for read on startup."""
|
|
|
|
storage.put(creds)
|
|
|
|
|
|
|
|
|
2022-02-01 02:14:49 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def mock_events_list(
|
2022-04-21 03:18:24 +00:00
|
|
|
aioclient_mock: AiohttpClientMocker,
|
|
|
|
) -> ApiResult:
|
2022-02-01 02:14:49 +00:00
|
|
|
"""Fixture to construct a fake event list API response."""
|
|
|
|
|
2022-04-21 03:18:24 +00:00
|
|
|
def _put_result(
|
2022-05-21 18:22:27 +00:00
|
|
|
response: dict[str, Any],
|
|
|
|
calendar_id: str = None,
|
|
|
|
exc: ClientError | None = None,
|
2022-04-21 03:18:24 +00:00
|
|
|
) -> None:
|
|
|
|
if calendar_id is None:
|
|
|
|
calendar_id = CALENDAR_ID
|
|
|
|
aioclient_mock.get(
|
|
|
|
f"{API_BASE_URL}/calendars/{calendar_id}/events",
|
|
|
|
json=response,
|
|
|
|
exc=exc,
|
2022-02-01 02:14:49 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
return _put_result
|
|
|
|
|
|
|
|
|
2022-02-27 18:58:00 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def mock_events_list_items(
|
|
|
|
mock_events_list: Callable[[dict[str, Any]], None]
|
|
|
|
) -> Callable[list[[dict[str, Any]]], None]:
|
|
|
|
"""Fixture to construct an API response containing event items."""
|
|
|
|
|
|
|
|
def _put_items(items: list[dict[str, Any]]) -> None:
|
|
|
|
mock_events_list({"items": items})
|
|
|
|
return
|
|
|
|
|
|
|
|
return _put_items
|
|
|
|
|
|
|
|
|
2022-02-01 02:14:49 +00:00
|
|
|
@pytest.fixture
|
|
|
|
def mock_calendars_list(
|
2022-04-21 03:18:24 +00:00
|
|
|
aioclient_mock: AiohttpClientMocker,
|
2022-02-01 02:14:49 +00:00
|
|
|
) -> ApiResult:
|
|
|
|
"""Fixture to construct a fake calendar list API response."""
|
|
|
|
|
2022-05-21 18:22:27 +00:00
|
|
|
def _result(response: dict[str, Any], exc: ClientError | None = None) -> None:
|
2022-04-21 03:18:24 +00:00
|
|
|
aioclient_mock.get(
|
|
|
|
f"{API_BASE_URL}/users/me/calendarList",
|
|
|
|
json=response,
|
|
|
|
exc=exc,
|
2022-02-01 02:14:49 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
|
2022-05-21 18:22:27 +00:00
|
|
|
return _result
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def mock_calendar_get(
|
|
|
|
aioclient_mock: AiohttpClientMocker,
|
|
|
|
) -> Callable[[...], None]:
|
|
|
|
"""Fixture for returning a calendar get response."""
|
|
|
|
|
|
|
|
def _result(
|
2022-06-21 13:42:41 +00:00
|
|
|
calendar_id: str,
|
|
|
|
response: dict[str, Any],
|
|
|
|
exc: ClientError | None = None,
|
|
|
|
status: http.HTTPStatus = http.HTTPStatus.OK,
|
2022-05-21 18:22:27 +00:00
|
|
|
) -> None:
|
|
|
|
aioclient_mock.get(
|
|
|
|
f"{API_BASE_URL}/calendars/{calendar_id}",
|
|
|
|
json=response,
|
|
|
|
exc=exc,
|
2022-06-21 13:42:41 +00:00
|
|
|
status=status,
|
2022-05-21 18:22:27 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
return _result
|
2022-02-01 02:14:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def mock_insert_event(
|
2022-04-21 03:18:24 +00:00
|
|
|
aioclient_mock: AiohttpClientMocker,
|
2022-05-21 18:22:27 +00:00
|
|
|
) -> Callable[[...], None]:
|
2022-04-21 03:18:24 +00:00
|
|
|
"""Fixture for capturing event creation."""
|
|
|
|
|
2022-07-08 03:50:19 +00:00
|
|
|
def _expect_result(
|
|
|
|
calendar_id: str = CALENDAR_ID, exc: ClientError | None = None
|
|
|
|
) -> None:
|
2022-04-21 03:18:24 +00:00
|
|
|
aioclient_mock.post(
|
|
|
|
f"{API_BASE_URL}/calendars/{calendar_id}/events",
|
2022-07-08 03:50:19 +00:00
|
|
|
exc=exc,
|
2022-04-21 03:18:24 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
return _expect_result
|
2022-03-04 07:12:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def set_time_zone(hass):
|
|
|
|
"""Set the time zone for the tests."""
|
|
|
|
# Set our timezone to CST/Regina so we can check calculations
|
|
|
|
# This keeps UTC-6 all year round
|
2022-03-20 09:25:15 +00:00
|
|
|
hass.config.set_time_zone("America/Regina")
|
2022-03-04 07:12:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def google_config_track_new() -> None:
|
|
|
|
"""Fixture for tests to set the 'track_new' configuration.yaml setting."""
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def google_config(google_config_track_new: bool | None) -> dict[str, Any]:
|
|
|
|
"""Fixture for overriding component config."""
|
2022-06-21 13:42:41 +00:00
|
|
|
google_config = {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET}
|
2022-03-04 07:12:24 +00:00
|
|
|
if google_config_track_new is not None:
|
|
|
|
google_config[CONF_TRACK_NEW] = google_config_track_new
|
|
|
|
return google_config
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-04-09 06:33:24 +00:00
|
|
|
def config(google_config: dict[str, Any]) -> dict[str, Any]:
|
2022-03-04 07:12:24 +00:00
|
|
|
"""Fixture for overriding component config."""
|
2022-05-14 17:27:47 +00:00
|
|
|
return {DOMAIN: google_config} if google_config else {}
|
2022-03-04 07:12:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-04-09 06:33:24 +00:00
|
|
|
def component_setup(hass: HomeAssistant, config: dict[str, Any]) -> ComponentSetup:
|
2022-03-04 07:12:24 +00:00
|
|
|
"""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
|