diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 032bd2fd36d..027320f16f8 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -36,6 +36,9 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_time +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -312,14 +315,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) - hass.http.register_view(CalendarListView(component)) - hass.http.register_view(CalendarEventView(component)) + frontend_loaded = False - frontend.async_register_built_in_panel(hass, "calendar", "calendar", "mdi:calendar") + @callback + def async_platform_loaded( + hass: HomeAssistant, integration_domain: str, platform: Any + ) -> None: + """Register frontend resources for calendar.""" + nonlocal frontend_loaded - websocket_api.async_register_command(hass, handle_calendar_event_create) - websocket_api.async_register_command(hass, handle_calendar_event_delete) - websocket_api.async_register_command(hass, handle_calendar_event_update) + if frontend_loaded: + return + + frontend_loaded = True + + hass.http.register_view(CalendarListView(component)) + hass.http.register_view(CalendarEventView(component)) + + frontend.async_register_built_in_panel( + hass, "calendar", "calendar", "mdi:calendar" + ) + + websocket_api.async_register_command(hass, handle_calendar_event_create) + websocket_api.async_register_command(hass, handle_calendar_event_delete) + websocket_api.async_register_command(hass, handle_calendar_event_update) + + await async_process_integration_platforms(hass, DOMAIN, async_platform_loaded) component.async_register_entity_service( CREATE_EVENT_SERVICE, diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index 19d0d9b1d29..0a3f0057ca7 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -125,12 +125,12 @@ async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: @pytest.fixture -def mock_setup_integration( +def mock_setup_config_entry_integration( hass: HomeAssistant, config_flow_fixture: None, test_entities: list[CalendarEntity], ) -> None: - """Fixture to set up a mock integration.""" + """Fixture to set up a mock integration with config entry.""" async def async_setup_entry_init( hass: HomeAssistant, config_entry: ConfigEntry diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 4945cddf9c9..6ca33ca5d66 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -7,7 +7,9 @@ from datetime import timedelta from http import HTTPStatus import re from typing import Any +from unittest.mock import AsyncMock +from aiohttp.test_utils import TestClient from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion @@ -20,14 +22,17 @@ from homeassistant.components.calendar import ( CalendarEntity, CalendarEntityDescription, ) +from homeassistant.const import CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported from homeassistant.helpers import entity_registry as er from homeassistant.helpers.typing import UNDEFINED +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from .conftest import MockCalendarEntity, MockConfigEntry +from tests.common import MockPlatform, mock_platform from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -50,12 +55,22 @@ def mock_set_frozen_time(frozen_time: str | None) -> Generator[None]: yield -@pytest.fixture(name="setup_platform", autouse=True) -async def mock_setup_platform( +@pytest.fixture(name="setup_calendar_integration") +async def mock_setup_calendar_integration( hass: HomeAssistant, set_time_zone: None, frozen_time: str | None, - mock_setup_integration: None, +) -> None: + """Fixture to setup the calendar integration.""" + await async_setup_component(hass, DOMAIN, {}) + + +@pytest.fixture(name="setup_calendar_config_entry_platform") +async def mock_setup_config_entry_platform( + hass: HomeAssistant, + set_time_zone: None, + frozen_time: str | None, + mock_setup_config_entry_integration: None, config_entry: MockConfigEntry, ) -> None: """Fixture to setup platforms used in the test and fixtures are set up in the right order.""" @@ -63,6 +78,7 @@ async def mock_setup_platform( await hass.async_block_till_done() +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_events_http_api( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: @@ -78,6 +94,7 @@ async def test_events_http_api( assert events[0]["summary"] == "Future Event" +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_events_http_api_missing_fields( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: @@ -87,6 +104,7 @@ async def test_events_http_api_missing_fields( assert response.status == HTTPStatus.BAD_REQUEST +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_events_http_api_error( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -106,6 +124,7 @@ async def test_events_http_api_error( assert await response.json() == {"message": "Error reading events: Failure"} +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_events_http_api_dates_wrong_order( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: @@ -119,6 +138,7 @@ async def test_events_http_api_dates_wrong_order( assert response.status == HTTPStatus.BAD_REQUEST +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_calendars_http_api( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: @@ -134,6 +154,7 @@ async def test_calendars_http_api( ] +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") @pytest.mark.parametrize( ("payload", "code"), [ @@ -222,6 +243,7 @@ async def test_unsupported_websocket( assert resp["error"].get("code") == code +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: """Test unsupported service call.""" with pytest.raises( @@ -242,6 +264,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("setup_calendar_integration") @pytest.mark.parametrize( ("date_fields", "expected_error", "error_match"), [ @@ -417,6 +440,7 @@ async def test_create_event_service_invalid_params( ) +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") @pytest.mark.parametrize( "frozen_time", ["2023-06-22 10:30:00+00:00"], ids=["frozen_time"] ) @@ -477,6 +501,7 @@ async def test_list_events_service( assert response == expected +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") @pytest.mark.parametrize( ("service"), [ @@ -516,6 +541,7 @@ async def test_list_events_service_duration( assert response == snapshot +@pytest.mark.usefixtures("setup_calendar_integration") async def test_list_events_positive_duration(hass: HomeAssistant) -> None: """Test listing events requires a positive duration.""" with pytest.raises(vol.Invalid, match="should be positive"): @@ -531,6 +557,7 @@ async def test_list_events_positive_duration(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("setup_calendar_integration") async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None: """Test listing events specifying fields that are exclusive.""" end = dt_util.now() + timedelta(days=1) @@ -549,6 +576,7 @@ async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("setup_calendar_integration") async def test_list_events_missing_fields(hass: HomeAssistant) -> None: """Test listing events missing some required fields.""" with pytest.raises(vol.Invalid, match="at least one of"): @@ -563,6 +591,7 @@ async def test_list_events_missing_fields(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("setup_calendar_integration") @pytest.mark.parametrize( "frozen_time", ["2023-06-22 10:30:00+00:00"], ids=["frozen_time"] ) @@ -613,6 +642,7 @@ async def test_list_events_service_same_dates( ) +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_calendar_initial_color_valid( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -628,6 +658,7 @@ async def test_calendar_initial_color_valid( assert entry.options.get(DOMAIN, {}).get("color") == "#FF0000" +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") @pytest.mark.parametrize( "invalid_initial_color", [ @@ -654,6 +685,7 @@ async def test_calendar_initial_color_invalid( assert entity.get_initial_entity_options() is None +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") async def test_calendar_initial_color_none( hass: HomeAssistant, test_entities: list[MockCalendarEntity], @@ -664,6 +696,7 @@ async def test_calendar_initial_color_none( assert entity.get_initial_entity_options() is None +@pytest.mark.usefixtures("setup_calendar_config_entry_platform") @pytest.mark.parametrize( ("description_color", "attr_color", "expected_color"), [ @@ -715,3 +748,79 @@ async def test_calendar_initial_color_precedence( entity = TestCalendarEntity(description_color, attr_color) assert entity.initial_color == expected_color + + +async def test_services_registered_after_integration_setup(hass: HomeAssistant) -> None: + """Test that services are registered after integration setup.""" + assert DOMAIN not in hass.services.async_services() + await async_setup_component(hass, DOMAIN, {}) + assert set(hass.services.async_services()[DOMAIN]) == {"create_event", "get_events"} + + +async def _assert_http_api_responses( + client: TestClient, + expected_status_calendar_list: HTTPStatus, + expected_status_calendar_event: HTTPStatus, +) -> None: + """Assert that the HTTP API endpoints return the expected status.""" + response = await client.get("/api/calendars") + assert response.status == expected_status_calendar_list + + response = await client.get("/api/calendars/calendar.calendar_1") + assert response.status == expected_status_calendar_event + + +async def test_frontend_resources_registered_after_first_config_entry_setup( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + set_time_zone: None, + frozen_time: str | None, + mock_setup_config_entry_integration: None, + config_entry: MockConfigEntry, +) -> None: + """Test that frontend resources are registered after the first config entry is set up.""" + await async_setup_component(hass, "http", {}) + client = await hass_client() + await _assert_http_api_responses(client, HTTPStatus.NOT_FOUND, HTTPStatus.NOT_FOUND) + assert "frontend_panels" not in hass.data + assert "websocket_api" not in hass.data + + await async_setup_component(hass, DOMAIN, {}) + await _assert_http_api_responses(client, HTTPStatus.NOT_FOUND, HTTPStatus.NOT_FOUND) + assert "frontend_panels" not in hass.data + assert "websocket_api" not in hass.data + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await _assert_http_api_responses(client, HTTPStatus.OK, HTTPStatus.BAD_REQUEST) + assert set(hass.data["frontend_panels"]) == {"calendar"} + assert set(hass.data["websocket_api"]) == { + "calendar/event/create", + "calendar/event/delete", + "calendar/event/update", + } + + +async def test_frontend_resources_registered_after_first_platform_setup( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, +) -> None: + """Test that frontend resources are registered after the first platform is set up.""" + await async_setup_component(hass, "http", {}) + client = await hass_client() + await _assert_http_api_responses(client, HTTPStatus.NOT_FOUND, HTTPStatus.NOT_FOUND) + assert "frontend_panels" not in hass.data + assert "websocket_api" not in hass.data + + mock_platform( + hass, + f"test.{DOMAIN}", + MockPlatform(async_setup_platform=AsyncMock()), + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await _assert_http_api_responses(client, HTTPStatus.OK, HTTPStatus.BAD_REQUEST) + assert set(hass.data["frontend_panels"]) == {"calendar"} + assert set(hass.data["websocket_api"]) == { + "calendar/event/create", + "calendar/event/delete", + "calendar/event/update", + } diff --git a/tests/components/calendar/test_recorder.py b/tests/components/calendar/test_recorder.py index c7511b8b2b0..715f87c28d1 100644 --- a/tests/components/calendar/test_recorder.py +++ b/tests/components/calendar/test_recorder.py @@ -19,7 +19,7 @@ async def mock_setup_dependencies( recorder_mock: Recorder, hass: HomeAssistant, set_time_zone: None, - mock_setup_integration: None, + mock_setup_config_entry_integration: None, config_entry: MockConfigEntry, ) -> None: """Fixture that ensures the recorder is setup in the right order.""" diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 8435f43f930..924cc3355d7 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -279,7 +279,7 @@ def mock_test_entity(test_entities: list[MockCalendarEntity]) -> MockCalendarEnt @pytest.fixture(name="setup_platform", autouse=True) async def mock_setup_platform( hass: HomeAssistant, - mock_setup_integration: None, + mock_setup_config_entry_integration: None, config_entry: MockConfigEntry, ) -> None: """Fixture to setup platforms used in the test."""