Compare commits

...

4 Commits

Author SHA1 Message Date
Erik
ce1e6e02f6 Format 2026-03-30 15:19:22 +02:00
Erik
69dcac1a19 Simplify 2026-03-30 15:15:18 +02:00
Erik
4bc29274c7 Simplify tests 2026-03-30 15:11:00 +02:00
Erik
cfc353be69 Set up calendar frontend resources when first platform is set up 2026-03-30 14:53:13 +02:00
5 changed files with 165 additions and 18 deletions

View File

@@ -37,7 +37,7 @@ 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.template import DATE_STR_FORMAT
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
@@ -308,19 +308,10 @@ SERVICE_GET_EVENTS_SCHEMA: Final = vol.All(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Track states and offer events for calendars."""
component = hass.data[DATA_COMPONENT] = EntityComponent[CalendarEntity](
component = hass.data[DATA_COMPONENT] = CalendarEntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
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)
component.async_register_entity_service(
CREATE_EVENT_SERVICE,
CREATE_EVENT_SCHEMA,
@@ -667,6 +658,53 @@ class CalendarEntity(Entity):
raise NotImplementedError
class CalendarEntityComponent(EntityComponent[CalendarEntity]):
"""Calendar entity component.
Sets up frontend resources and websocket API when the first platform is added.
"""
_frontend_loaded: bool = False
async def async_setup_entry(self, config_entry: ConfigEntry) -> bool:
"""Set up a config entry."""
result = await super().async_setup_entry(config_entry)
if not self._frontend_loaded:
self._register_frontend_resources()
return result
async def async_setup_platform(
self,
platform_type: str,
platform_config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a platform for this component."""
await super().async_setup_platform(
platform_type, platform_config, discovery_info
)
if not self._frontend_loaded:
self._register_frontend_resources()
def _register_frontend_resources(self) -> None:
"""Register frontend resources for calendar."""
self._frontend_loaded = True
self.hass.http.register_view(CalendarListView(self))
self.hass.http.register_view(CalendarEventView(self))
frontend.async_register_built_in_panel(
self.hass, "calendar", "calendar", "mdi:calendar"
)
websocket_api.async_register_command(self.hass, handle_calendar_event_create)
websocket_api.async_register_command(self.hass, handle_calendar_event_delete)
websocket_api.async_register_command(self.hass, handle_calendar_event_update)
class CalendarEventView(http.HomeAssistantView):
"""View to retrieve calendar content."""

View File

@@ -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

View File

@@ -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",
}

View File

@@ -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."""

View File

@@ -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."""