Simplify google calendar authentication to combine some of the cases together, and reduce unecessarily checks. Make the tests share common authentication setup and reduce use of mocks by introducing a fake for holding on to credentials. This makes future refactoring simpler, so we don't have to care as much about the interactions with the credentials storage.
457 lines
16 KiB
Python
457 lines
16 KiB
Python
"""The tests for the google calendar platform."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from http import HTTPStatus
|
|
from typing import Any
|
|
from unittest.mock import Mock, patch
|
|
|
|
import httplib2
|
|
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.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
|
|
|
|
from .conftest import TEST_CALENDAR
|
|
|
|
from tests.common import async_mock_service
|
|
|
|
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 = {
|
|
"summary": "Test All Day Event",
|
|
"start": {},
|
|
"end": {},
|
|
"location": "Test Cases",
|
|
"description": "test event",
|
|
"kind": "calendar#event",
|
|
"created": "2016-06-23T16:37:57.000Z",
|
|
"transparency": "transparent",
|
|
"updated": "2016-06-24T01:57:21.045Z",
|
|
"reminders": {"useDefault": True},
|
|
"organizer": {
|
|
"email": "uvrttabwegnui4gtia3vyqb@import.calendar.google.com",
|
|
"displayName": "Organizer Name",
|
|
"self": True,
|
|
},
|
|
"sequence": 0,
|
|
"creator": {
|
|
"email": "uvrttabwegnui4gtia3vyqb@import.calendar.google.com",
|
|
"displayName": "Organizer Name",
|
|
"self": True,
|
|
},
|
|
"id": "_c8rinwq863h45qnucyoi43ny8",
|
|
"etag": '"2933466882090000"',
|
|
"htmlLink": "https://www.google.com/calendar/event?eid=*******",
|
|
"iCalUID": "cydrevtfuybguinhomj@google.com",
|
|
"status": "confirmed",
|
|
}
|
|
|
|
|
|
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)
|
|
def mock_google_setup(hass, test_calendar, mock_token_read):
|
|
"""Mock the google set up functions."""
|
|
hass.loop.run_until_complete(async_setup_component(hass, "group", {"group": {}}))
|
|
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"))
|
|
|
|
|
|
@pytest.fixture(name="google_service")
|
|
def mock_google_service():
|
|
"""Mock google service."""
|
|
patch_google_service = patch(
|
|
"homeassistant.components.google.calendar.GoogleCalendarService"
|
|
)
|
|
with patch_google_service as mock_service:
|
|
yield mock_service
|
|
|
|
|
|
async def test_all_day_event(hass, mock_next_event):
|
|
"""Test that we can create an event trigger on device."""
|
|
week_from_today = dt_util.dt.date.today() + dt_util.dt.timedelta(days=7)
|
|
end_event = week_from_today + dt_util.dt.timedelta(days=1)
|
|
event = copy.deepcopy(TEST_EVENT)
|
|
start = week_from_today.isoformat()
|
|
end = end_event.isoformat()
|
|
event["start"]["date"] = start
|
|
event["end"]["date"] = end
|
|
mock_next_event.return_value.event = event
|
|
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": True,
|
|
"offset_reached": False,
|
|
"start_time": week_from_today.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
async def test_future_event(hass, mock_next_event):
|
|
"""Test that we can create an event trigger on device."""
|
|
one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30)
|
|
end_event = one_hour_from_now + dt_util.dt.timedelta(minutes=60)
|
|
start = one_hour_from_now.isoformat()
|
|
end = end_event.isoformat()
|
|
event = copy.deepcopy(TEST_EVENT)
|
|
event["start"]["dateTime"] = start
|
|
event["end"]["dateTime"] = end
|
|
mock_next_event.return_value.event = event
|
|
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": False,
|
|
"offset_reached": False,
|
|
"start_time": one_hour_from_now.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
async def test_in_progress_event(hass, mock_next_event):
|
|
"""Test that we can create an event trigger on device."""
|
|
middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30)
|
|
end_event = middle_of_event + dt_util.dt.timedelta(minutes=60)
|
|
start = middle_of_event.isoformat()
|
|
end = end_event.isoformat()
|
|
event = copy.deepcopy(TEST_EVENT)
|
|
event["start"]["dateTime"] = start
|
|
event["end"]["dateTime"] = end
|
|
mock_next_event.return_value.event = event
|
|
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_ON
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": False,
|
|
"offset_reached": False,
|
|
"start_time": middle_of_event.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
async def test_offset_in_progress_event(hass, mock_next_event):
|
|
"""Test that we can create an event trigger on device."""
|
|
middle_of_event = dt_util.now() + dt_util.dt.timedelta(minutes=14)
|
|
end_event = middle_of_event + dt_util.dt.timedelta(minutes=60)
|
|
start = middle_of_event.isoformat()
|
|
end = end_event.isoformat()
|
|
event_summary = "Test Event in Progress"
|
|
event = copy.deepcopy(TEST_EVENT)
|
|
event["start"]["dateTime"] = start
|
|
event["end"]["dateTime"] = end
|
|
event["summary"] = f"{event_summary} !!-15"
|
|
mock_next_event.return_value.event = event
|
|
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event_summary,
|
|
"all_day": False,
|
|
"offset_reached": True,
|
|
"start_time": middle_of_event.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
@pytest.mark.skip
|
|
async def test_all_day_offset_in_progress_event(hass, mock_next_event):
|
|
"""Test that we can create an event trigger on device."""
|
|
tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=1)
|
|
end_event = tomorrow + dt_util.dt.timedelta(days=1)
|
|
start = tomorrow.isoformat()
|
|
end = end_event.isoformat()
|
|
event_summary = "Test All Day Event Offset In Progress"
|
|
event = copy.deepcopy(TEST_EVENT)
|
|
event["start"]["date"] = start
|
|
event["end"]["date"] = end
|
|
event["summary"] = f"{event_summary} !!-25:0"
|
|
mock_next_event.return_value.event = event
|
|
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event_summary,
|
|
"all_day": True,
|
|
"offset_reached": True,
|
|
"start_time": tomorrow.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
async def test_all_day_offset_event(hass, mock_next_event):
|
|
"""Test that we can create an event trigger on device."""
|
|
tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=2)
|
|
end_event = tomorrow + dt_util.dt.timedelta(days=1)
|
|
start = tomorrow.isoformat()
|
|
end = end_event.isoformat()
|
|
offset_hours = 1 + dt_util.now().hour
|
|
event_summary = "Test All Day Event Offset"
|
|
event = copy.deepcopy(TEST_EVENT)
|
|
event["start"]["date"] = start
|
|
event["end"]["date"] = end
|
|
event["summary"] = f"{event_summary} !!-{offset_hours}:0"
|
|
mock_next_event.return_value.event = event
|
|
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event_summary,
|
|
"all_day": True,
|
|
"offset_reached": False,
|
|
"start_time": tomorrow.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
async def test_update_error(hass, google_service):
|
|
"""Test that the calendar handles a server error."""
|
|
google_service.return_value.get = Mock(
|
|
side_effect=httplib2.ServerNotFoundError("unit test")
|
|
)
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == "off"
|
|
|
|
|
|
async def test_calendars_api(hass, hass_client, google_service):
|
|
"""Test the Rest API returns the calendar."""
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
client = await hass_client()
|
|
response = await client.get("/api/calendars")
|
|
assert response.status == HTTPStatus.OK
|
|
data = await response.json()
|
|
assert data == [
|
|
{
|
|
"entity_id": TEST_ENTITY,
|
|
"name": TEST_ENTITY_NAME,
|
|
}
|
|
]
|
|
|
|
|
|
async def test_http_event_api_failure(hass, hass_client, google_service):
|
|
"""Test the Rest API response during a calendar failure."""
|
|
google_service.return_value.get = Mock(
|
|
side_effect=httplib2.ServerNotFoundError("unit test")
|
|
)
|
|
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
start = dt_util.now().isoformat()
|
|
end = (dt_util.now() + dt_util.dt.timedelta(minutes=60)).isoformat()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}")
|
|
assert response.status == HTTPStatus.OK
|
|
# A failure to talk to the server results in an empty list of events
|
|
events = await response.json()
|
|
assert events == []
|
|
|
|
|
|
async def test_http_api_event(hass, hass_client, google_service, mock_events_list):
|
|
"""Test querying the API and fetching events from the server."""
|
|
now = dt_util.now()
|
|
|
|
mock_events_list(
|
|
{
|
|
"items": [
|
|
{
|
|
"summary": "Event title",
|
|
"start": {"dateTime": now.isoformat()},
|
|
"end": {
|
|
"dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat()
|
|
},
|
|
}
|
|
],
|
|
}
|
|
)
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
start = (now - dt_util.dt.timedelta(minutes=60)).isoformat()
|
|
end = (now + dt_util.dt.timedelta(minutes=60)).isoformat()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}")
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert len(events) == 1
|
|
assert "summary" in events[0]
|
|
assert events[0]["summary"] == "Event title"
|
|
|
|
|
|
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("test_calendar", [create_ignore_avail_calendar()])
|
|
async def test_opaque_event(hass, hass_client, google_service, mock_events_list):
|
|
"""Test querying the API and fetching events from the server."""
|
|
now = dt_util.now()
|
|
|
|
mock_events_list(
|
|
{
|
|
"items": [
|
|
{
|
|
"summary": "Event title",
|
|
"transparency": "opaque",
|
|
"start": {"dateTime": now.isoformat()},
|
|
"end": {
|
|
"dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat()
|
|
},
|
|
}
|
|
],
|
|
}
|
|
)
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
start = (now - dt_util.dt.timedelta(minutes=60)).isoformat()
|
|
end = (now + dt_util.dt.timedelta(minutes=60)).isoformat()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}")
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert len(events) == 1
|
|
assert "summary" in events[0]
|
|
assert events[0]["summary"] == "Event title"
|
|
|
|
|
|
@pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()])
|
|
async def test_transparent_event(hass, hass_client, google_service, mock_events_list):
|
|
"""Test querying the API and fetching events from the server."""
|
|
now = dt_util.now()
|
|
|
|
mock_events_list(
|
|
{
|
|
"items": [
|
|
{
|
|
"summary": "Event title",
|
|
"transparency": "transparent",
|
|
"start": {"dateTime": now.isoformat()},
|
|
"end": {
|
|
"dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat()
|
|
},
|
|
}
|
|
],
|
|
}
|
|
)
|
|
assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG})
|
|
await hass.async_block_till_done()
|
|
|
|
start = (now - dt_util.dt.timedelta(minutes=60)).isoformat()
|
|
end = (now + dt_util.dt.timedelta(minutes=60)).isoformat()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}")
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert events == []
|