diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index f224f3f2f31..5e1411c76c8 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,25 +1,44 @@ """Test configuration and mocks for the google integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable import datetime 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 oauth2client.client import Credentials, OAuth2Credentials 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.setup import async_setup_component +import homeassistant.util.dt as dt_util from homeassistant.util.dt import utcnow +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + ApiResult = Callable[[dict[str, Any]], None] +ComponentSetup = Callable[[], Awaitable[bool]] T = TypeVar("T") YieldFixture = Generator[T, None, None] 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, "etag": '"3584134138943410"', "timeZone": "UTC", @@ -32,14 +51,63 @@ TEST_CALENDAR = { "summary": "We are, we are, a... Test Calendar", "colorId": "8", "defaultReminders": [], - "track": True, } @pytest.fixture -def test_calendar(): - """Return a test calendar.""" - return TEST_CALENDAR +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], + } + ] + + +@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: @@ -156,3 +224,49 @@ def mock_insert_event( insert_mock = Mock() calendar_resource.return_value.events.return_value.insert = 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 diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index dda30a1d83e..76ab20ff41b 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -2,38 +2,22 @@ from __future__ import annotations +import datetime from http import HTTPStatus from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import Mock 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 .conftest import TEST_YAML_ENTITY, TEST_YAML_ENTITY_NAME -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_ENTITY = TEST_YAML_ENTITY +TEST_ENTITY_NAME = TEST_YAML_ENTITY_NAME TEST_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) -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")) +def mock_test_setup(mock_calendars_yaml, mock_token_read): + """Fixture that pulls in the default fixtures for tests in this file.""" + return def upcoming() -> dict[str, Any]: @@ -114,22 +60,24 @@ def upcoming() -> dict[str, Any]: now = dt_util.now() return { "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: """Return a calendar API to return events created by upcoming().""" now = dt_util.now() - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + start = (now - datetime.timedelta(minutes=60)).isoformat() + end = (now + datetime.timedelta(minutes=60)).isoformat() 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.""" - 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) + week_from_today = dt_util.now().date() + datetime.timedelta(days=7) + end_event = week_from_today + datetime.timedelta(days=1) event = { **TEST_EVENT, "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]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) 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.""" - 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) + one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) + end_event = one_hour_from_now + datetime.timedelta(minutes=60) event = { **TEST_EVENT, "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]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) 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.""" - middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30) - end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) + middle_of_event = dt_util.now() - datetime.timedelta(minutes=30) + end_event = middle_of_event + datetime.timedelta(minutes=60) event = { **TEST_EVENT, "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]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) 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.""" - middle_of_event = dt_util.now() + dt_util.dt.timedelta(minutes=14) - end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) + middle_of_event = dt_util.now() + datetime.timedelta(minutes=14) + end_event = middle_of_event + datetime.timedelta(minutes=60) event_summary = "Test Event in Progress" event = { **TEST_EVENT, @@ -226,8 +171,7 @@ async def test_offset_in_progress_event(hass, mock_events_list_items): } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) 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(hass, mock_events_list_items): +async def test_all_day_offset_in_progress_event( + hass, mock_events_list_items, component_setup +): """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) + tomorrow = dt_util.now().date() + datetime.timedelta(days=1) + end_event = tomorrow + datetime.timedelta(days=1) event_summary = "Test All Day Event Offset In Progress" 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]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) 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.""" - tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=2) - end_event = tomorrow + dt_util.dt.timedelta(days=1) - offset_hours = 1 + dt_util.now().hour + now = dt_util.now() + day_after_tomorrow = now.date() + datetime.timedelta(days=2) + end_event = day_after_tomorrow + datetime.timedelta(days=1) + offset_hours = 1 + now.hour event_summary = "Test All Day Event Offset" event = { **TEST_EVENT, - "start": {"date": tomorrow.isoformat()}, + "start": {"date": day_after_tomorrow.isoformat()}, "end": {"date": end_event.isoformat()}, "summary": f"{event_summary} !!-{offset_hours}:0", } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) 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, "all_day": True, "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), "location": event["location"], "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.""" calendar_resource.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() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME 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.""" - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() 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.""" calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test") - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() 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 == [] -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.""" event = { **TEST_EVENT, **upcoming(), } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() 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"] -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,transparency,expect_visible_event", + "calendars_config_ignore_availability,transparency,expect_visible_event", [ - (create_ignore_avail_calendar(), "opaque", True), - (create_ignore_avail_calendar(), "transparent", False), + # Look at visibility to determine if entity is created + (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( - 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.""" event = { @@ -396,8 +345,7 @@ async def test_opaque_event( "transparency": transparency, } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() response = await client.get(upcoming_event_url()) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 4baa577851f..a0766c8256e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,8 +1,10 @@ """The tests for the Google Calendar component.""" +from __future__ import annotations + from collections.abc import Awaitable, Callable import datetime from typing import Any -from unittest.mock import Mock, call, mock_open, patch +from unittest.mock import Mock, call, patch from oauth2client.client import ( FlowExchangeError, @@ -10,21 +12,26 @@ from oauth2client.client import ( OAuth2DeviceCodeError, ) import pytest -import yaml from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT -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.util import dt as dt_util +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant, State 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 # Typing helpers -ComponentSetup = Callable[[], Awaitable[bool]] HassApi = Callable[[], Awaitable[dict[str, Any]]] CODE_CHECK_INTERVAL = 1 @@ -59,35 +66,6 @@ async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[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 async def mock_notification() -> YieldFixture[Mock]: """Fixture for capturing persistent notifications.""" @@ -95,26 +73,6 @@ async def mock_notification() -> YieldFixture[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): """Fire an alarm and wait for callbacks to run.""" 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() - 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( @@ -151,9 +119,9 @@ async def test_init_success( now = utcnow() 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.name == "Backyard Light" + assert state.name == TEST_YAML_ENTITY_NAME assert state.state == STATE_OFF mock_notification.assert_called() @@ -174,7 +142,7 @@ async def test_code_error( ): assert await component_setup() - assert not hass.states.get("calendar.backyard_light") + assert not hass.states.get(TEST_YAML_ENTITY) mock_notification.assert_called() assert "Error: Test Failure" in mock_notification.call_args[0][1] @@ -194,7 +162,7 @@ async def test_expired_after_exchange( now = utcnow() 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() assert ( @@ -220,7 +188,7 @@ async def test_exchange_error( now = utcnow() 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() 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.""" assert await component_setup() - state = hass.states.get("calendar.backyard_light") + state = hass.states.get(TEST_YAML_ENTITY) assert state - assert state.name == "Backyard Light" + assert state.name == TEST_YAML_ENTITY_NAME assert state.state == STATE_OFF 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) 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.name == "Backyard Light" + assert state.name == TEST_YAML_ENTITY_NAME assert state.state == STATE_OFF # 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.""" 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() @@ -306,38 +274,133 @@ async def test_invalid_calendar_yaml( # Integration fails to 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() -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, mock_token_read: None, component_setup: ComponentSetup, 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: """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([])) - 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") + # The calendar does not + state = hass.states.get(TEST_API_ENTITY) assert state - assert state.name == "We are, we are, a... Test Calendar" + assert state.name == TEST_API_ENTITY_NAME 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( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], mock_insert_event: Mock, ) -> None: """Test service call that adds an event.""" @@ -387,7 +450,7 @@ async def test_add_event_date_in_x( mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], mock_insert_event: Mock, date_fields: dict[str, Any], 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, mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], mock_insert_event: Mock, ) -> None: """Test service call that sets a date range.""" assert await component_setup() - now = dt_util.utcnow() + now = utcnow() today = now.date() end_date = today + datetime.timedelta(days=2) @@ -464,3 +526,50 @@ async def test_add_event_date_range( "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", + }, + }, + )