core/tests/components/google/test_init.py

951 lines
28 KiB
Python

"""The tests for the Google Calendar component."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
import datetime
import http
import time
from typing import Any
from unittest.mock import Mock, patch
import zoneinfo
from aiohttp.client_exceptions import ClientError
import pytest
import voluptuous as vol
from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT
from homeassistant.components.google.calendar import SERVICE_CREATE_EVENT
from homeassistant.components.google.const import CONF_CALENDAR_ACCESS
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.dt import UTC, utcnow
from .conftest import (
CALENDAR_ID,
EMAIL_ADDRESS,
TEST_API_ENTITY,
TEST_API_ENTITY_NAME,
TEST_YAML_ENTITY,
ApiResult,
ComponentSetup,
)
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
EXPIRED_TOKEN_TIMESTAMP = datetime.datetime(2022, 4, 8).timestamp()
# Typing helpers
type HassApi = Callable[[], Awaitable[dict[str, Any]]]
TEST_EVENT_SUMMARY = "Test Summary"
TEST_EVENT_DESCRIPTION = "Test Description"
TEST_EVENT_LOCATION = "Test Location"
def assert_state(actual: State | None, expected: State | None) -> None:
"""Assert that the two states are equal."""
if actual is None or expected is None:
assert actual == expected
return
assert actual.entity_id == expected.entity_id
assert actual.state == expected.state
assert actual.attributes == expected.attributes
@pytest.fixture(
params=[
(
DOMAIN,
SERVICE_ADD_EVENT,
{"calendar_id": CALENDAR_ID},
None,
),
(
DOMAIN,
SERVICE_CREATE_EVENT,
{},
{"entity_id": TEST_API_ENTITY},
),
(
"calendar",
SERVICE_CREATE_EVENT,
{},
{"entity_id": TEST_API_ENTITY},
),
],
ids=("google.add_event", "google.create_event", "calendar.create_event"),
)
def add_event_call_service(
hass: HomeAssistant,
request: pytest.FixtureRequest,
) -> Callable[[dict[str, Any]], Awaitable[None]]:
"""Fixture for calling the add or create event service."""
(domain, service_call, data, target) = request.param
async def call_service(params: dict[str, Any]) -> None:
await hass.services.async_call(
domain,
service_call,
{
**data,
**params,
"summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION,
},
target=target,
blocking=True,
)
return call_service
async def test_unload_entry(
hass: HomeAssistant,
component_setup: ComponentSetup,
) -> None:
"""Test load and unload of a ConfigEntry."""
await component_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
entry = entries[0]
assert entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
assert entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
"token_scopes", ["https://www.googleapis.com/auth/calendar.readonly"]
)
async def test_existing_token_missing_scope(
hass: HomeAssistant,
token_scopes: list[str],
component_setup: ComponentSetup,
config_entry: MockConfigEntry,
) -> None:
"""Test setup where existing token does not have sufficient scopes."""
await component_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
@pytest.mark.parametrize("config_entry_options", [{CONF_CALENDAR_ACCESS: "read_only"}])
async def test_config_entry_scope_reauth(
hass: HomeAssistant,
token_scopes: list[str],
component_setup: ComponentSetup,
config_entry: MockConfigEntry,
) -> None:
"""Test setup where the config entry options requires reauth to match the scope."""
await component_setup()
assert config_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
@pytest.mark.parametrize("calendars_config", [[{"cal_id": "invalid-schema"}]])
async def test_calendar_yaml_missing_required_fields(
hass: HomeAssistant,
component_setup: ComponentSetup,
calendars_config: list[dict[str, Any]],
mock_calendars_yaml: None,
config_entry: MockConfigEntry,
) -> None:
"""Test setup with a missing schema fields, ignores the error and continues."""
assert not await component_setup()
assert config_entry.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize("calendars_config", [[{"missing-cal_id": "invalid-schema"}]])
async def test_invalid_calendar_yaml(
hass: HomeAssistant,
component_setup: ComponentSetup,
calendars_config: list[dict[str, Any]],
mock_calendars_yaml: None,
config_entry: MockConfigEntry,
) -> None:
"""Test setup with missing entity id fields fails to load the platform."""
assert not await component_setup()
assert config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_calendar_yaml_error(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
) -> None:
"""Test setup with yaml file not found."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
with patch("homeassistant.components.google.open", side_effect=FileNotFoundError()):
assert await component_setup()
assert not hass.states.get(TEST_YAML_ENTITY)
assert hass.states.get(TEST_API_ENTITY)
@pytest.mark.parametrize("calendars_config", [None])
async def test_empty_calendar_yaml(
hass: HomeAssistant,
component_setup: ComponentSetup,
calendars_config: list[dict[str, Any]],
mock_calendars_yaml: None,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
) -> None:
"""Test an empty yaml file is equivalent to a missing yaml file."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
assert not hass.states.get(TEST_YAML_ENTITY)
assert hass.states.get(TEST_API_ENTITY)
async def test_init_calendar(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
) -> None:
"""Test finding a calendar from the API."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
state = hass.states.get(TEST_API_ENTITY)
assert state
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)
async def test_multiple_config_entries(
hass: HomeAssistant,
component_setup: ComponentSetup,
config_entry: MockConfigEntry,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test finding a calendar from the API."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
state = hass.states.get(TEST_API_ENTITY)
assert state
assert state.state == STATE_OFF
assert state.attributes.get(ATTR_FRIENDLY_NAME) == TEST_API_ENTITY_NAME
config_entry2 = MockConfigEntry(
domain=DOMAIN, data=config_entry.data, unique_id="other-address@example.com"
)
calendar2 = {
**test_api_calendar,
"id": "calendar-id2",
"summary": "Example Calendar 2",
}
aioclient_mock.clear_requests()
mock_calendars_list({"items": [calendar2]})
mock_events_list({}, calendar_id="calendar-id2")
config_entry2.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry2.entry_id)
await hass.async_block_till_done()
state = hass.states.get("calendar.example_calendar_2")
assert state
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 2"
@pytest.mark.parametrize(
("date_fields", "expected_error", "error_match"),
[
(
{},
vol.error.MultipleInvalid,
"must contain at least one of start_date, start_date_time, in",
),
(
{
"start_date": "2022-04-01",
},
vol.error.MultipleInvalid,
"Start and end dates must both be specified",
),
(
{
"end_date": "2022-04-02",
},
vol.error.MultipleInvalid,
"must contain at least one of start_date, start_date_time, in.",
),
(
{
"start_date_time": "2022-04-01T06:00:00",
},
vol.error.MultipleInvalid,
"Start and end datetimes must both be specified",
),
(
{
"end_date_time": "2022-04-02T07:00:00",
},
vol.error.MultipleInvalid,
"must contain at least one of start_date, start_date_time, in.",
),
(
{
"start_date": "2022-04-01",
"start_date_time": "2022-04-01T06:00:00",
"end_date_time": "2022-04-02T07:00:00",
},
vol.error.MultipleInvalid,
"must contain at most one of start_date, start_date_time, in.",
),
(
{
"start_date_time": "2022-04-01T06:00:00",
"end_date_time": "2022-04-01T07:00:00",
"end_date": "2022-04-02",
},
vol.error.MultipleInvalid,
"Start and end dates must both be specified",
),
(
{
"start_date": "2022-04-01",
"end_date_time": "2022-04-02T07:00:00",
},
vol.error.MultipleInvalid,
"Start and end dates must both be specified",
),
(
{
"start_date_time": "2022-04-01T07:00:00",
"end_date": "2022-04-02",
},
vol.error.MultipleInvalid,
"Start and end dates must both be specified",
),
(
{
"in": {
"days": 2,
"weeks": 2,
}
},
vol.error.MultipleInvalid,
"two or more values in the same group of exclusion 'event_types'",
),
(
{
"start_date": "2022-04-01",
"end_date": "2022-04-02",
"in": {
"days": 2,
},
},
vol.error.MultipleInvalid,
"must contain at most one of start_date, start_date_time, in.",
),
(
{
"start_date_time": "2022-04-01T07:00:00",
"end_date_time": "2022-04-01T07:00:00",
"in": {
"days": 2,
},
},
vol.error.MultipleInvalid,
"must contain at most one of start_date, start_date_time, in.",
),
],
ids=[
"missing_all",
"missing_end_date",
"missing_start_date",
"missing_end_datetime",
"missing_start_datetime",
"multiple_start",
"multiple_end",
"missing_end_date",
"missing_end_date_time",
"multiple_in",
"unexpected_in_with_date",
"unexpected_in_with_datetime",
],
)
async def test_add_event_invalid_params(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]],
date_fields: dict[str, Any],
expected_error: type[Exception],
error_match: str | None,
) -> None:
"""Test service calls with incorrect fields."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
with pytest.raises(expected_error, match=error_match):
await add_event_call_service(date_fields)
@pytest.mark.parametrize(
("date_fields", "start_timedelta", "end_timedelta"),
[
(
{"in": {"days": 3}},
datetime.timedelta(days=3),
datetime.timedelta(days=4),
),
(
{"in": {"weeks": 1}},
datetime.timedelta(days=7),
datetime.timedelta(days=8),
),
],
ids=["in_days", "in_weeks"],
)
async def test_add_event_date_in_x(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
mock_insert_event: Callable[..., None],
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
date_fields: dict[str, Any],
start_timedelta: datetime.timedelta,
end_timedelta: datetime.timedelta,
aioclient_mock: AiohttpClientMocker,
add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]],
) -> None:
"""Test service call that adds an event with various time ranges."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
now = datetime.datetime.now()
start_date = now + start_timedelta
end_date = now + end_timedelta
aioclient_mock.clear_requests()
mock_insert_event(
calendar_id=CALENDAR_ID,
)
await add_event_call_service(date_fields)
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == {
"summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION,
"start": {"date": start_date.date().isoformat()},
"end": {"date": end_date.date().isoformat()},
}
async def test_add_event_date(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_insert_event: Callable[..., None],
mock_events_list: ApiResult,
aioclient_mock: AiohttpClientMocker,
add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]],
) -> None:
"""Test service call that sets a date range."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
now = utcnow()
today = now.date()
end_date = today + datetime.timedelta(days=2)
aioclient_mock.clear_requests()
mock_insert_event(
calendar_id=CALENDAR_ID,
)
await add_event_call_service(
{
"start_date": today.isoformat(),
"end_date": end_date.isoformat(),
},
)
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == {
"summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION,
"start": {"date": today.isoformat()},
"end": {"date": end_date.isoformat()},
}
async def test_add_event_date_time(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
mock_insert_event: Callable[..., None],
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
aioclient_mock: AiohttpClientMocker,
add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]],
) -> None:
"""Test service call that adds an event with a date time range."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina"))
delta = datetime.timedelta(days=3, hours=3)
end_datetime = start_datetime + delta
aioclient_mock.clear_requests()
mock_insert_event(
calendar_id=CALENDAR_ID,
)
await add_event_call_service(
{
"start_date_time": start_datetime.isoformat(),
"end_date_time": end_datetime.isoformat(),
},
)
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == {
"summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION,
"start": {
"dateTime": start_datetime.isoformat(timespec="seconds"),
"timeZone": "America/Regina",
},
"end": {
"dateTime": end_datetime.isoformat(timespec="seconds"),
"timeZone": "America/Regina",
},
}
@pytest.mark.parametrize(
"calendars_config",
[
[
{
"cal_id": CALENDAR_ID,
"entities": [
{
"device_id": "backyard_light",
"name": "Backyard Light",
"search": "#Backyard",
},
],
}
],
],
)
async def test_unsupported_create_event(
hass: HomeAssistant,
mock_calendars_yaml: Mock,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
mock_insert_event: Callable[..., None],
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test create event service call is unsupported for virtual calendars."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina"))
delta = datetime.timedelta(days=3, hours=3)
end_datetime = start_datetime + delta
with pytest.raises(HomeAssistantError, match="does not support this service"):
await hass.services.async_call(
DOMAIN,
"create_event",
{
# **data,
"start_date_time": start_datetime.isoformat(),
"end_date_time": end_datetime.isoformat(),
"summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION,
},
target={"entity_id": "calendar.backyard_light"},
blocking=True,
)
async def test_add_event_failure(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
mock_insert_event: Callable[..., None],
add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]],
) -> None:
"""Test service calls with incorrect fields."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
mock_insert_event(
calendar_id=CALENDAR_ID,
exc=ClientError(),
)
with pytest.raises(HomeAssistantError):
await add_event_call_service(
{"start_date": "2022-05-01", "end_date": "2022-05-02"}
)
async def test_add_event_location(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_insert_event: Callable[..., None],
mock_events_list: ApiResult,
aioclient_mock: AiohttpClientMocker,
add_event_call_service: Callable[[dict[str, Any]], Awaitable[None]],
) -> None:
"""Test service call that sets a location field."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
now = utcnow()
today = now.date()
end_date = today + datetime.timedelta(days=2)
aioclient_mock.clear_requests()
mock_insert_event(
calendar_id=CALENDAR_ID,
)
await add_event_call_service(
{
"start_date": today.isoformat(),
"end_date": end_date.isoformat(),
"location": TEST_EVENT_LOCATION,
},
)
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == {
"summary": TEST_EVENT_SUMMARY,
"description": TEST_EVENT_DESCRIPTION,
"location": TEST_EVENT_LOCATION,
"start": {"date": today.isoformat()},
"end": {"date": end_date.isoformat()},
}
@pytest.mark.parametrize(
"config_entry_token_expiry",
[
(datetime.datetime.max.replace(tzinfo=UTC).timestamp() + 1),
(utcnow().replace(tzinfo=None).timestamp()),
],
ids=["max_timestamp", "timestamp_naive"],
)
async def test_invalid_token_expiry_in_config_entry(
hass: HomeAssistant,
component_setup: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Exercise case in issue #69623 with invalid token expiration persisted."""
# The token is refreshed and new expiration values are returned
expires_in = 86400
expires_at = time.time() + expires_in
aioclient_mock.post(
"https://oauth2.googleapis.com/token",
json={
"refresh_token": "some-refresh-token",
"access_token": "some-updated-token",
"expires_at": expires_at,
"expires_in": expires_in,
},
)
assert await component_setup()
# Verify token expiration values are updated
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.LOADED
assert entries[0].data["token"]["access_token"] == "some-updated-token"
assert entries[0].data["token"]["expires_in"] == expires_in
@pytest.mark.parametrize("config_entry_token_expiry", [EXPIRED_TOKEN_TIMESTAMP])
async def test_expired_token_refresh_internal_error(
hass: HomeAssistant,
component_setup: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Generic errors on reauth are treated as a retryable setup error."""
aioclient_mock.post(
"https://oauth2.googleapis.com/token",
status=http.HTTPStatus.INTERNAL_SERVER_ERROR,
)
await component_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize(
"config_entry_token_expiry",
[EXPIRED_TOKEN_TIMESTAMP],
)
async def test_expired_token_requires_reauth(
hass: HomeAssistant,
component_setup: ComponentSetup,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test case where reauth is required for token that cannot be refreshed."""
aioclient_mock.post(
"https://oauth2.googleapis.com/token",
status=http.HTTPStatus.BAD_REQUEST,
)
await component_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
assert entries[0].state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
@pytest.mark.parametrize(
("calendars_config", "expect_write_calls"),
[
(
[
{
"cal_id": "ignored",
"entities": {"device_id": "existing", "name": "existing"},
}
],
True,
),
([], False),
],
ids=["has_yaml", "no_yaml"],
)
async def test_calendar_yaml_update(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_yaml: Mock,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
calendars_config: dict[str, Any],
expect_write_calls: bool,
) -> None:
"""Test updating the yaml file with a new calendar."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
mock_calendars_yaml().read.assert_called()
assert mock_calendars_yaml().write.called is expect_write_calls
state = hass.states.get(TEST_API_ENTITY)
assert state
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)
async def test_update_will_reload(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
config_entry: MockConfigEntry,
) -> None:
"""Test updating config entry options will trigger a reload."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
await component_setup()
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.options == {} # read_write is default
with patch(
"homeassistant.config_entries.ConfigEntries.async_reload",
return_value=None,
) as mock_reload:
# No-op does not reload
hass.config_entries.async_update_entry(
config_entry, options={CONF_CALENDAR_ACCESS: "read_write"}
)
await hass.async_block_till_done()
mock_reload.assert_not_called()
# Data change does not trigger reload
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
"example": "field",
},
)
await hass.async_block_till_done()
mock_reload.assert_not_called()
# Reload when options changed
hass.config_entries.async_update_entry(
config_entry, options={CONF_CALENDAR_ACCESS: "read_only"}
)
await hass.async_block_till_done()
mock_reload.assert_called_once()
@pytest.mark.parametrize("config_entry_unique_id", [None])
async def test_assign_unique_id(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
mock_calendar_get: Callable[..., None],
config_entry: MockConfigEntry,
) -> None:
"""Test an existing config is updated to have unique id if it does not exist."""
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert config_entry.unique_id is None
mock_calendar_get(
"primary",
{"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "owner"},
)
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id == EMAIL_ADDRESS
@pytest.mark.parametrize(
("config_entry_unique_id", "request_status", "config_entry_status"),
[
(None, http.HTTPStatus.BAD_REQUEST, ConfigEntryState.SETUP_RETRY),
(
None,
http.HTTPStatus.UNAUTHORIZED,
ConfigEntryState.SETUP_ERROR,
),
],
)
async def test_assign_unique_id_failure(
hass: HomeAssistant,
component_setup: ComponentSetup,
mock_calendars_list: ApiResult,
test_api_calendar: dict[str, Any],
config_entry: MockConfigEntry,
mock_events_list: ApiResult,
mock_calendar_get: Callable[..., None],
request_status: http.HTTPStatus,
config_entry_status: ConfigEntryState,
) -> None:
"""Test lookup failures during unique id assignment are handled gracefully."""
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert config_entry.unique_id is None
mock_calendar_get(
"primary",
{},
status=request_status,
)
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
await component_setup()
assert config_entry.state is config_entry_status
assert config_entry.unique_id is None
async def test_remove_entry(
hass: HomeAssistant,
mock_calendars_list: ApiResult,
component_setup: ComponentSetup,
test_api_calendar: dict[str, Any],
mock_events_list: ApiResult,
) -> None:
"""Test load and remove of a ConfigEntry."""
mock_calendars_list({"items": [test_api_calendar]})
mock_events_list({})
assert await component_setup()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
entry = entries[0]
assert entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_remove(entry.entry_id)
assert entry.state is ConfigEntryState.NOT_LOADED