Improve google calendar test quality and share setup (#67441)

* Improve google calendar test quality and share setup

Improve google calendar test quality by exercising different combinations of
cases and new coverage. The existing test cases do achieve very high coverage,
however some of the subtle interactions between the components are not as well
exercised, which is needed when we start changing how the internal code is
structured moving to async or to config entries.

Ipmrovement include:
- Exercising additional cases around different types of configuration parameters
  that control how calendars are tracked or not tracked. The tracking can be
  configured in google_calendars.yaml, or by defaults set in configuration.yaml
  for new calendars.
- Share even more test setup, used when exercising the above different scenarios
- Add new test cases for event creation.
- Improve test readability by making more clear the differences between tests
  exercising yaml and API test calendars. The data types are now more clearly
  separated in the two cases, as well as the entity names created in the two
  cases.

* Undo some diffs for readability

* Improve pydoc readability

* Improve pydoc readability

* Incorporate improvements from Martin

* Make test parameters a State instance

* Update test naming to be more correct

* Fix flake8 errors
This commit is contained in:
Allen Porter 2022-03-03 23:12:24 -08:00 committed by GitHub
parent 4e52f26ed1
commit 57ffc65af2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 385 additions and 214 deletions

View File

@ -1,25 +1,44 @@
"""Test configuration and mocks for the google integration.""" """Test configuration and mocks for the google integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Awaitable, Callable
import datetime import datetime
from typing import Any, Generator, TypeVar from typing import Any, Generator, TypeVar
from unittest.mock import Mock, patch from unittest.mock import Mock, mock_open, patch
from googleapiclient import discovery as google_discovery from googleapiclient import discovery as google_discovery
from oauth2client.client import Credentials, OAuth2Credentials from oauth2client.client import Credentials, OAuth2Credentials
import pytest import pytest
import yaml
from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
ApiResult = Callable[[dict[str, Any]], None] ApiResult = Callable[[dict[str, Any]], None]
ComponentSetup = Callable[[], Awaitable[bool]]
T = TypeVar("T") T = TypeVar("T")
YieldFixture = Generator[T, None, None] YieldFixture = Generator[T, None, None]
CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com" CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com"
TEST_CALENDAR = {
# 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 = {
"id": CALENDAR_ID, "id": CALENDAR_ID,
"etag": '"3584134138943410"', "etag": '"3584134138943410"',
"timeZone": "UTC", "timeZone": "UTC",
@ -32,14 +51,63 @@ TEST_CALENDAR = {
"summary": "We are, we are, a... Test Calendar", "summary": "We are, we are, a... Test Calendar",
"colorId": "8", "colorId": "8",
"defaultReminders": [], "defaultReminders": [],
"track": True,
} }
@pytest.fixture @pytest.fixture
def test_calendar(): def test_api_calendar():
"""Return a test calendar.""" """Return a test calendar object used in API responses."""
return TEST_CALENDAR 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],
}
]
@pytest.fixture
async def mock_calendars_yaml(
hass: HomeAssistant,
calendars_config: list[dict[str, Any]],
) -> None:
"""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):
yield
class FakeStorage: class FakeStorage:
@ -156,3 +224,49 @@ def mock_insert_event(
insert_mock = Mock() insert_mock = Mock()
calendar_resource.return_value.events.return_value.insert = insert_mock calendar_resource.return_value.events.return_value.insert = insert_mock
return insert_mock return insert_mock
@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
hass.config.time_zone = "CST"
dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina"))
yield
dt_util.set_default_time_zone(ORIG_TIMEZONE)
@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."""
google_config = {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-secret"}
if google_config_track_new is not None:
google_config[CONF_TRACK_NEW] = google_config_track_new
return google_config
@pytest.fixture
async def config(google_config: dict[str, Any]) -> dict[str, Any]:
"""Fixture for overriding component config."""
return {DOMAIN: google_config}
@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

View File

@ -2,38 +2,22 @@
from __future__ import annotations from __future__ import annotations
import datetime
from http import HTTPStatus from http import HTTPStatus
from typing import Any from typing import Any
from unittest.mock import Mock, patch from unittest.mock import Mock
import httplib2 import httplib2
import pytest import pytest
from homeassistant.components.google import (
CONF_CAL_ID,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_DEVICE_ID,
CONF_ENTITIES,
CONF_IGNORE_AVAILABILITY,
CONF_NAME,
CONF_TRACK,
DEVICE_SCHEMA,
SERVICE_SCAN_CALENDARS,
)
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.setup import async_setup_component
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .conftest import TEST_CALENDAR from .conftest import TEST_YAML_ENTITY, TEST_YAML_ENTITY_NAME
from tests.common import async_mock_service TEST_ENTITY = TEST_YAML_ENTITY
TEST_ENTITY_NAME = TEST_YAML_ENTITY_NAME
GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"}
TEST_ENTITY = "calendar.we_are_we_are_a_test_calendar"
TEST_ENTITY_NAME = "We are, we are, a... Test Calendar"
TEST_EVENT = { TEST_EVENT = {
"summary": "Test All Day Event", "summary": "Test All Day Event",
@ -65,48 +49,10 @@ TEST_EVENT = {
} }
def get_calendar_info(calendar):
"""Convert data from Google into DEVICE_SCHEMA."""
calendar_info = DEVICE_SCHEMA(
{
CONF_CAL_ID: calendar["id"],
CONF_ENTITIES: [
{
CONF_TRACK: calendar["track"],
CONF_NAME: calendar["summary"],
CONF_DEVICE_ID: slugify(calendar["summary"]),
CONF_IGNORE_AVAILABILITY: calendar.get("ignore_availability", True),
}
],
}
)
return calendar_info
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_google_setup(hass, test_calendar, mock_token_read): def mock_test_setup(mock_calendars_yaml, mock_token_read):
"""Mock the google set up functions.""" """Fixture that pulls in the default fixtures for tests in this file."""
hass.loop.run_until_complete(async_setup_component(hass, "group", {"group": {}})) return
calendar = get_calendar_info(test_calendar)
calendars = {calendar[CONF_CAL_ID]: calendar}
patch_google_load = patch(
"homeassistant.components.google.load_config", return_value=calendars
)
patch_google_services = patch("homeassistant.components.google.setup_services")
async_mock_service(hass, "google", SERVICE_SCAN_CALENDARS)
with patch_google_load, patch_google_services:
yield
@pytest.fixture(autouse=True)
def set_time_zone():
"""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
dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina"))
yield
dt_util.set_default_time_zone(dt_util.get_time_zone("UTC"))
def upcoming() -> dict[str, Any]: def upcoming() -> dict[str, Any]:
@ -114,22 +60,24 @@ def upcoming() -> dict[str, Any]:
now = dt_util.now() now = dt_util.now()
return { return {
"start": {"dateTime": now.isoformat()}, "start": {"dateTime": now.isoformat()},
"end": {"dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat()}, "end": {"dateTime": (now + datetime.timedelta(minutes=5)).isoformat()},
} }
def upcoming_event_url() -> str: def upcoming_event_url() -> str:
"""Return a calendar API to return events created by upcoming().""" """Return a calendar API to return events created by upcoming()."""
now = dt_util.now() now = dt_util.now()
start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() start = (now - datetime.timedelta(minutes=60)).isoformat()
end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() end = (now + datetime.timedelta(minutes=60)).isoformat()
return f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}" return f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}"
async def test_all_day_event(hass, mock_events_list_items, mock_token_read): async def test_all_day_event(
hass, mock_events_list_items, mock_token_read, component_setup
):
"""Test that we can create an event trigger on device.""" """Test that we can create an event trigger on device."""
week_from_today = dt_util.dt.date.today() + dt_util.dt.timedelta(days=7) week_from_today = dt_util.now().date() + datetime.timedelta(days=7)
end_event = week_from_today + dt_util.dt.timedelta(days=1) end_event = week_from_today + datetime.timedelta(days=1)
event = { event = {
**TEST_EVENT, **TEST_EVENT,
"start": {"date": week_from_today.isoformat()}, "start": {"date": week_from_today.isoformat()},
@ -137,8 +85,7 @@ async def test_all_day_event(hass, mock_events_list_items, mock_token_read):
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY) state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME assert state.name == TEST_ENTITY_NAME
@ -155,10 +102,10 @@ async def test_all_day_event(hass, mock_events_list_items, mock_token_read):
} }
async def test_future_event(hass, mock_events_list_items): async def test_future_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test that we can create an event trigger on device."""
one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
end_event = one_hour_from_now + dt_util.dt.timedelta(minutes=60) end_event = one_hour_from_now + datetime.timedelta(minutes=60)
event = { event = {
**TEST_EVENT, **TEST_EVENT,
"start": {"dateTime": one_hour_from_now.isoformat()}, "start": {"dateTime": one_hour_from_now.isoformat()},
@ -166,8 +113,7 @@ async def test_future_event(hass, mock_events_list_items):
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY) state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME assert state.name == TEST_ENTITY_NAME
@ -184,10 +130,10 @@ async def test_future_event(hass, mock_events_list_items):
} }
async def test_in_progress_event(hass, mock_events_list_items): async def test_in_progress_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test that we can create an event trigger on device."""
middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30) middle_of_event = dt_util.now() - datetime.timedelta(minutes=30)
end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) end_event = middle_of_event + datetime.timedelta(minutes=60)
event = { event = {
**TEST_EVENT, **TEST_EVENT,
"start": {"dateTime": middle_of_event.isoformat()}, "start": {"dateTime": middle_of_event.isoformat()},
@ -195,8 +141,7 @@ async def test_in_progress_event(hass, mock_events_list_items):
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY) state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME assert state.name == TEST_ENTITY_NAME
@ -213,10 +158,10 @@ async def test_in_progress_event(hass, mock_events_list_items):
} }
async def test_offset_in_progress_event(hass, mock_events_list_items): async def test_offset_in_progress_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test that we can create an event trigger on device."""
middle_of_event = dt_util.now() + dt_util.dt.timedelta(minutes=14) middle_of_event = dt_util.now() + datetime.timedelta(minutes=14)
end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) end_event = middle_of_event + datetime.timedelta(minutes=60)
event_summary = "Test Event in Progress" event_summary = "Test Event in Progress"
event = { event = {
**TEST_EVENT, **TEST_EVENT,
@ -226,8 +171,7 @@ async def test_offset_in_progress_event(hass, mock_events_list_items):
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY) state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME assert state.name == TEST_ENTITY_NAME
@ -244,11 +188,12 @@ async def test_offset_in_progress_event(hass, mock_events_list_items):
} }
@pytest.mark.skip async def test_all_day_offset_in_progress_event(
async def test_all_day_offset_in_progress_event(hass, mock_events_list_items): hass, mock_events_list_items, component_setup
):
"""Test that we can create an event trigger on device.""" """Test that we can create an event trigger on device."""
tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=1) tomorrow = dt_util.now().date() + datetime.timedelta(days=1)
end_event = tomorrow + dt_util.dt.timedelta(days=1) end_event = tomorrow + datetime.timedelta(days=1)
event_summary = "Test All Day Event Offset In Progress" event_summary = "Test All Day Event Offset In Progress"
event = { event = {
**TEST_EVENT, **TEST_EVENT,
@ -258,8 +203,7 @@ async def test_all_day_offset_in_progress_event(hass, mock_events_list_items):
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY) state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME assert state.name == TEST_ENTITY_NAME
@ -276,22 +220,22 @@ async def test_all_day_offset_in_progress_event(hass, mock_events_list_items):
} }
async def test_all_day_offset_event(hass, mock_events_list_items): async def test_all_day_offset_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test that we can create an event trigger on device."""
tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=2) now = dt_util.now()
end_event = tomorrow + dt_util.dt.timedelta(days=1) day_after_tomorrow = now.date() + datetime.timedelta(days=2)
offset_hours = 1 + dt_util.now().hour end_event = day_after_tomorrow + datetime.timedelta(days=1)
offset_hours = 1 + now.hour
event_summary = "Test All Day Event Offset" event_summary = "Test All Day Event Offset"
event = { event = {
**TEST_EVENT, **TEST_EVENT,
"start": {"date": tomorrow.isoformat()}, "start": {"date": day_after_tomorrow.isoformat()},
"end": {"date": end_event.isoformat()}, "end": {"date": end_event.isoformat()},
"summary": f"{event_summary} !!-{offset_hours}:0", "summary": f"{event_summary} !!-{offset_hours}:0",
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY) state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME assert state.name == TEST_ENTITY_NAME
@ -301,30 +245,28 @@ async def test_all_day_offset_event(hass, mock_events_list_items):
"message": event_summary, "message": event_summary,
"all_day": True, "all_day": True,
"offset_reached": False, "offset_reached": False,
"start_time": tomorrow.strftime(DATE_STR_FORMAT), "start_time": day_after_tomorrow.strftime(DATE_STR_FORMAT),
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
} }
async def test_update_error(hass, calendar_resource): async def test_update_error(hass, calendar_resource, component_setup):
"""Test that the calendar handles a server error.""" """Test that the calendar handles a server error."""
calendar_resource.return_value.get = Mock( calendar_resource.return_value.get = Mock(
side_effect=httplib2.ServerNotFoundError("unit test") side_effect=httplib2.ServerNotFoundError("unit test")
) )
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY) state = hass.states.get(TEST_ENTITY)
assert state.name == TEST_ENTITY_NAME assert state.name == TEST_ENTITY_NAME
assert state.state == "off" assert state.state == "off"
async def test_calendars_api(hass, hass_client): async def test_calendars_api(hass, hass_client, component_setup):
"""Test the Rest API returns the calendar.""" """Test the Rest API returns the calendar."""
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
response = await client.get("/api/calendars") response = await client.get("/api/calendars")
@ -338,12 +280,13 @@ async def test_calendars_api(hass, hass_client):
] ]
async def test_http_event_api_failure(hass, hass_client, calendar_resource): async def test_http_event_api_failure(
hass, hass_client, calendar_resource, component_setup
):
"""Test the Rest API response during a calendar failure.""" """Test the Rest API response during a calendar failure."""
calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test") calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test")
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
response = await client.get(upcoming_event_url()) response = await client.get(upcoming_event_url())
@ -353,15 +296,16 @@ async def test_http_event_api_failure(hass, hass_client, calendar_resource):
assert events == [] assert events == []
async def test_http_api_event(hass, hass_client, mock_events_list_items): async def test_http_api_event(
hass, hass_client, mock_events_list_items, component_setup
):
"""Test querying the API and fetching events from the server.""" """Test querying the API and fetching events from the server."""
event = { event = {
**TEST_EVENT, **TEST_EVENT,
**upcoming(), **upcoming(),
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
response = await client.get(upcoming_event_url()) response = await client.get(upcoming_event_url())
@ -372,22 +316,27 @@ async def test_http_api_event(hass, hass_client, mock_events_list_items):
assert events[0]["summary"] == event["summary"] assert events[0]["summary"] == event["summary"]
def create_ignore_avail_calendar() -> dict[str, Any]:
"""Create a calendar with ignore_availability set."""
calendar = TEST_CALENDAR.copy()
calendar["ignore_availability"] = False
return calendar
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_calendar,transparency,expect_visible_event", "calendars_config_ignore_availability,transparency,expect_visible_event",
[ [
(create_ignore_avail_calendar(), "opaque", True), # Look at visibility to determine if entity is created
(create_ignore_avail_calendar(), "transparent", False), (False, "opaque", True),
(False, "transparent", False),
# Ignoring availability and always show the entity
(True, "opaque", True),
(True, "transparency", True),
# Default to ignore availability
(None, "opaque", True),
(None, "transparency", True),
], ],
) )
async def test_opaque_event( async def test_opaque_event(
hass, hass_client, mock_events_list_items, transparency, expect_visible_event hass,
hass_client,
mock_events_list_items,
component_setup,
transparency,
expect_visible_event,
): ):
"""Test querying the API and fetching events from the server.""" """Test querying the API and fetching events from the server."""
event = { event = {
@ -396,8 +345,7 @@ async def test_opaque_event(
"transparency": transparency, "transparency": transparency,
} }
mock_events_list_items([event]) mock_events_list_items([event])
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) assert await component_setup()
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
response = await client.get(upcoming_event_url()) response = await client.get(upcoming_event_url())

View File

@ -1,8 +1,10 @@
"""The tests for the Google Calendar component.""" """The tests for the Google Calendar component."""
from __future__ import annotations
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import datetime import datetime
from typing import Any from typing import Any
from unittest.mock import Mock, call, mock_open, patch from unittest.mock import Mock, call, patch
from oauth2client.client import ( from oauth2client.client import (
FlowExchangeError, FlowExchangeError,
@ -10,21 +12,26 @@ from oauth2client.client import (
OAuth2DeviceCodeError, OAuth2DeviceCodeError,
) )
import pytest import pytest
import yaml
from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_OFF from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, State
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .conftest import CALENDAR_ID, ApiResult, YieldFixture from .conftest import (
CALENDAR_ID,
TEST_API_ENTITY,
TEST_API_ENTITY_NAME,
TEST_YAML_ENTITY,
TEST_YAML_ENTITY_NAME,
ApiResult,
ComponentSetup,
YieldFixture,
)
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
# Typing helpers # Typing helpers
ComponentSetup = Callable[[], Awaitable[bool]]
HassApi = Callable[[], Awaitable[dict[str, Any]]] HassApi = Callable[[], Awaitable[dict[str, Any]]]
CODE_CHECK_INTERVAL = 1 CODE_CHECK_INTERVAL = 1
@ -59,35 +66,6 @@ async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]:
yield mock yield mock
@pytest.fixture
async def calendars_config() -> list[dict[str, Any]]:
"""Fixture for tests to override default calendar configuration."""
return [
{
"cal_id": CALENDAR_ID,
"entities": [
{
"device_id": "backyard_light",
"name": "Backyard Light",
"search": "#Backyard",
"track": True,
}
],
}
]
@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 @pytest.fixture
async def mock_notification() -> YieldFixture[Mock]: async def mock_notification() -> YieldFixture[Mock]:
"""Fixture for capturing persistent notifications.""" """Fixture for capturing persistent notifications."""
@ -95,26 +73,6 @@ async def mock_notification() -> YieldFixture[Mock]:
yield 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
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):
@ -133,7 +91,17 @@ async def test_setup_config_empty(
mock_notification.assert_not_called() mock_notification.assert_not_called()
assert not hass.states.get("calendar.backyard_light") 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."""
if actual is None:
assert actual == expected
return
assert actual.entity_id == expected.entity_id
assert actual.state == expected.state
assert actual.attributes == expected.attributes
async def test_init_success( async def test_init_success(
@ -151,9 +119,9 @@ async def test_init_success(
now = utcnow() now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
state = hass.states.get("calendar.backyard_light") state = hass.states.get(TEST_YAML_ENTITY)
assert state assert state
assert state.name == "Backyard Light" assert state.name == TEST_YAML_ENTITY_NAME
assert state.state == STATE_OFF assert state.state == STATE_OFF
mock_notification.assert_called() mock_notification.assert_called()
@ -174,7 +142,7 @@ async def test_code_error(
): ):
assert await component_setup() assert await component_setup()
assert not hass.states.get("calendar.backyard_light") assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_called() mock_notification.assert_called()
assert "Error: Test Failure" in mock_notification.call_args[0][1] assert "Error: Test Failure" in mock_notification.call_args[0][1]
@ -194,7 +162,7 @@ async def test_expired_after_exchange(
now = utcnow() now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
assert not hass.states.get("calendar.backyard_light") assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_called() mock_notification.assert_called()
assert ( assert (
@ -220,7 +188,7 @@ async def test_exchange_error(
now = utcnow() now = utcnow()
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
assert not hass.states.get("calendar.backyard_light") assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_called() mock_notification.assert_called()
assert "In order to authorize Home-Assistant" in mock_notification.call_args[0][1] assert "In order to authorize Home-Assistant" in mock_notification.call_args[0][1]
@ -236,9 +204,9 @@ async def test_existing_token(
"""Test setup with an existing token file.""" """Test setup with an existing token file."""
assert await component_setup() assert await component_setup()
state = hass.states.get("calendar.backyard_light") state = hass.states.get(TEST_YAML_ENTITY)
assert state assert state
assert state.name == "Backyard Light" assert state.name == TEST_YAML_ENTITY_NAME
assert state.state == STATE_OFF assert state.state == STATE_OFF
mock_notification.assert_not_called() mock_notification.assert_not_called()
@ -265,9 +233,9 @@ async def test_existing_token_missing_scope(
await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA)
assert len(mock_exchange.mock_calls) == 1 assert len(mock_exchange.mock_calls) == 1
state = hass.states.get("calendar.backyard_light") state = hass.states.get(TEST_YAML_ENTITY)
assert state assert state
assert state.name == "Backyard Light" assert state.name == TEST_YAML_ENTITY_NAME
assert state.state == STATE_OFF assert state.state == STATE_OFF
# No notifications on success # No notifications on success
@ -287,7 +255,7 @@ async def test_calendar_yaml_missing_required_fields(
"""Test setup with a missing schema fields, ignores the error and continues.""" """Test setup with a missing schema fields, ignores the error and continues."""
assert await component_setup() assert await component_setup()
assert not hass.states.get("calendar.backyard_light") assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_not_called() mock_notification.assert_not_called()
@ -306,38 +274,133 @@ async def test_invalid_calendar_yaml(
# Integration fails to setup # Integration fails to setup
assert not await component_setup() assert not await component_setup()
assert not hass.states.get("calendar.backyard_light") assert not hass.states.get(TEST_YAML_ENTITY)
mock_notification.assert_not_called() mock_notification.assert_not_called()
async def test_found_calendar_from_api( @pytest.mark.parametrize(
"google_config_track_new,calendars_config,expected_state",
[
(
None,
[],
State(
TEST_API_ENTITY,
STATE_OFF,
attributes={
"offset_reached": False,
"friendly_name": TEST_API_ENTITY_NAME,
},
),
),
(
True,
[],
State(
TEST_API_ENTITY,
STATE_OFF,
attributes={
"offset_reached": False,
"friendly_name": TEST_API_ENTITY_NAME,
},
),
),
(False, [], None),
],
ids=["default", "True", "False"],
)
async def test_track_new(
hass: HomeAssistant, hass: HomeAssistant,
mock_token_read: None, mock_token_read: None,
component_setup: ComponentSetup, component_setup: ComponentSetup,
mock_calendars_list: ApiResult, mock_calendars_list: ApiResult,
test_calendar: dict[str, Any], test_api_calendar: dict[str, Any],
mock_calendars_yaml: None,
expected_state: State,
) -> None:
"""Test behavior of configuration.yaml settings for tracking new calendars not in the config."""
mock_calendars_list({"items": [test_api_calendar]})
assert await component_setup()
# The calendar does not
state = hass.states.get(TEST_API_ENTITY)
assert_state(state, expected_state)
@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],
) -> None: ) -> None:
"""Test finding a calendar from the API.""" """Test finding a calendar from the API."""
mock_calendars_list({"items": [test_calendar]}) mock_calendars_list({"items": [test_api_calendar]})
assert await component_setup()
mocked_open_function = mock_open(read_data=yaml.dump([])) # The calendar does not
with patch("homeassistant.components.google.open", mocked_open_function): state = hass.states.get(TEST_API_ENTITY)
assert await component_setup()
state = hass.states.get("calendar.we_are_we_are_a_test_calendar")
assert state assert state
assert state.name == "We are, we are, a... Test Calendar" assert state.name == TEST_API_ENTITY_NAME
assert state.state == STATE_OFF assert state.state == STATE_OFF
# No yaml config loaded that overwrites the entity name
assert not hass.states.get(TEST_YAML_ENTITY)
@pytest.mark.parametrize(
"calendars_config_track,expected_state",
[
(
True,
State(
TEST_YAML_ENTITY,
STATE_OFF,
attributes={
"offset_reached": False,
"friendly_name": TEST_YAML_ENTITY_NAME,
},
),
),
(False, None),
],
)
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,
) -> None:
"""Test calendar config that overrides whether or not a calendar is tracked."""
mock_calendars_list({"items": [test_api_calendar]})
assert await component_setup()
state = hass.states.get(TEST_YAML_ENTITY)
assert_state(state, expected_state)
if calendars_config_track:
assert state
assert state.name == TEST_YAML_ENTITY_NAME
assert state.state == STATE_OFF
else:
assert not state
async def test_add_event( async def test_add_event(
hass: HomeAssistant, hass: HomeAssistant,
mock_token_read: None, mock_token_read: None,
component_setup: ComponentSetup, component_setup: ComponentSetup,
mock_calendars_list: ApiResult, mock_calendars_list: ApiResult,
test_calendar: dict[str, Any], test_api_calendar: dict[str, Any],
mock_insert_event: Mock, mock_insert_event: Mock,
) -> None: ) -> None:
"""Test service call that adds an event.""" """Test service call that adds an event."""
@ -387,7 +450,7 @@ async def test_add_event_date_in_x(
mock_token_read: None, mock_token_read: None,
component_setup: ComponentSetup, component_setup: ComponentSetup,
mock_calendars_list: ApiResult, mock_calendars_list: ApiResult,
test_calendar: dict[str, Any], test_api_calendar: dict[str, Any],
mock_insert_event: Mock, mock_insert_event: Mock,
date_fields: dict[str, Any], date_fields: dict[str, Any],
start_timedelta: datetime.timedelta, start_timedelta: datetime.timedelta,
@ -425,19 +488,18 @@ async def test_add_event_date_in_x(
) )
async def test_add_event_date_range( async def test_add_event_date(
hass: HomeAssistant, hass: HomeAssistant,
mock_token_read: None, mock_token_read: None,
component_setup: ComponentSetup, component_setup: ComponentSetup,
mock_calendars_list: ApiResult, mock_calendars_list: ApiResult,
test_calendar: dict[str, Any],
mock_insert_event: Mock, mock_insert_event: Mock,
) -> None: ) -> None:
"""Test service call that sets a date range.""" """Test service call that sets a date range."""
assert await component_setup() assert await component_setup()
now = dt_util.utcnow() now = utcnow()
today = now.date() today = now.date()
end_date = today + datetime.timedelta(days=2) end_date = today + datetime.timedelta(days=2)
@ -464,3 +526,50 @@ async def test_add_event_date_range(
"end": {"date": end_date.isoformat()}, "end": {"date": end_date.isoformat()},
}, },
) )
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,
) -> None:
"""Test service call that adds an event with a date time range."""
assert await component_setup()
start_datetime = datetime.datetime.now()
delta = datetime.timedelta(days=3, hours=3)
end_datetime = start_datetime + delta
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_EVENT,
{
"calendar_id": CALENDAR_ID,
"summary": "Summary",
"description": "Description",
"start_date_time": start_datetime.isoformat(),
"end_date_time": end_datetime.isoformat(),
},
blocking=True,
)
mock_insert_event.assert_called()
assert mock_insert_event.mock_calls[0] == call(
calendarId=CALENDAR_ID,
body={
"summary": "Summary",
"description": "Description",
"start": {
"dateTime": start_datetime.isoformat(timespec="seconds"),
"timeZone": "CST",
},
"end": {
"dateTime": end_datetime.isoformat(timespec="seconds"),
"timeZone": "CST",
},
},
)