diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 70732af1fd8..f034d48b9c5 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -300,6 +300,7 @@ async def async_setup_services( calendars = await hass.async_add_executor_job( load_config, hass.config.path(YAML_DEVICES) ) + calendars_file_lock = asyncio.Lock() async def _found_calendar(calendar_item: Calendar) -> None: calendar = get_calendar_info( @@ -307,15 +308,19 @@ async def async_setup_services( calendar_item.dict(exclude_unset=True), ) calendar_id = calendar_item.id - # Populate the yaml file with all discovered calendars - if calendar_id not in calendars: - calendars[calendar_id] = calendar - await hass.async_add_executor_job( - update_config, hass.config.path(YAML_DEVICES), calendar - ) - else: - # Prefer entity/name information from yaml, overriding api - calendar = calendars[calendar_id] + # If the google_calendars.yaml file already exists, populate it for + # backwards compatibility, but otherwise do not create it if it does + # not exist. + if calendars: + if calendar_id not in calendars: + calendars[calendar_id] = calendar + async with calendars_file_lock: + await hass.async_add_executor_job( + update_config, hass.config.path(YAML_DEVICES), calendar + ) + else: + # Prefer entity/name information from yaml, overriding api + calendar = calendars[calendar_id] async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar) created_calendars = set() diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 48badbc3ab5..b5566450913 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime from typing import Any, Generator, TypeVar -from unittest.mock import mock_open, patch +from unittest.mock import Mock, mock_open, patch from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL @@ -104,11 +104,11 @@ def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, def mock_calendars_yaml( hass: HomeAssistant, calendars_config: list[dict[str, Any]], -) -> None: +) -> Generator[Mock, None, 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 + yield mocked_open_function class FakeStorage: @@ -170,10 +170,17 @@ def config_entry_token_expiry(token_expiry: datetime.datetime) -> float: return token_expiry.timestamp() +@pytest.fixture +def config_entry_options() -> dict[str, Any] | None: + """Fixture to set initial config entry options.""" + return None + + @pytest.fixture def config_entry( token_scopes: list[str], config_entry_token_expiry: float, + config_entry_options: dict[str, Any] | None, ) -> MockConfigEntry: """Fixture to create a config entry for the integration.""" return MockConfigEntry( @@ -188,6 +195,7 @@ def config_entry( "expires_at": config_entry_token_expiry, }, }, + options=config_entry_options, ) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 6ae62f50914..794065ca09a 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -572,14 +572,16 @@ async def test_opaque_event( assert (len(events) > 0) == expect_visible_event +@pytest.mark.parametrize("mock_test_setup", [None]) async def test_scan_calendar_error( hass, component_setup, test_api_calendar, mock_calendars_list, + config_entry, ): """Test that the calendar update handles a server error.""" - + config_entry.add_to_hass(hass) mock_calendars_list({}, exc=ClientError()) assert await component_setup() diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 4709379e840..93c0642514e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -6,7 +6,7 @@ import datetime import http import time from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -19,6 +19,7 @@ from homeassistant.components.google import ( SERVICE_ADD_EVENT, SERVICE_SCAN_CALENDARS, ) +from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, State @@ -229,7 +230,10 @@ async def test_found_calendar_from_api( assert not hass.states.get(TEST_YAML_ENTITY) -@pytest.mark.parametrize("calendars_config,google_config", [([], {})]) +@pytest.mark.parametrize( + "calendars_config,google_config,config_entry_options", + [([], {}, {CONF_CALENDAR_ACCESS: "read_write"})], +) async def test_load_application_credentials( hass: HomeAssistant, component_setup: ComponentSetup, @@ -604,3 +608,48 @@ async def test_expired_token_requires_reauth( 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, + setup_config_entry: MockConfigEntry, + 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() + 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)