"""The tests for the Google Calendar component.""" 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 from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol from homeassistant.components.google import DOMAIN 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, ServiceNotSupported from homeassistant.helpers.config_entry_oauth2_flow import ( ImplementationUnavailableError, ) from homeassistant.setup import async_setup_component 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_CREATE_EVENT, {}, {"entity_id": TEST_API_ENTITY}, ), ( "calendar", SERVICE_CREATE_EVENT, {}, {"entity_id": TEST_API_ENTITY}, ), ], ids=("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]], freezer: FrozenDateTimeFactory, ) -> 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() # Freeze at 2025-06-15 03:00 UTC which is 2025-06-14 21:00 in America/Regina # (the test timezone). This ensures dt_util.now().date() returns June 14 # while a naive datetime.now().date() would return June 15 in UTC. freezer.move_to("2025-06-15 03:00:00+00:00") today = datetime.date(2025, 6, 14) start_date = today + start_timedelta end_date = today + 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.isoformat()}, "end": {"date": end_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.""" await async_setup_component(hass, "homeassistant", {}) 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 entity_id = "calendar.backyard_light" with pytest.raises( ServiceNotSupported, match=f"Entity {entity_id} does not support action google.create_event", ): 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": entity_id}, 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) @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 async def test_oauth_implementation_not_available( hass: HomeAssistant, config_entry: MockConfigEntry, ) -> None: """Test that unavailable OAuth implementation raises ConfigEntryNotReady.""" config_entry.add_to_hass(hass) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", side_effect=ImplementationUnavailableError, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_RETRY