From 26582cecbd4c899a8364f340b79c530e40dfb29d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Aug 2025 17:32:52 +0200 Subject: [PATCH] Improve test of REST endpoint /api/services (#150897) --- tests/components/api/snapshots/test_init.ambr | 144 ++++++++++++++++++ tests/components/api/test_init.py | 73 ++++++++- 2 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 tests/components/api/snapshots/test_init.ambr diff --git a/tests/components/api/snapshots/test_init.ambr b/tests/components/api/snapshots/test_init.ambr new file mode 100644 index 00000000000..05b6bf31638 --- /dev/null +++ b/tests/components/api/snapshots/test_init.ambr @@ -0,0 +1,144 @@ +# serializer version: 1 +# name: test_api_get_services + list([ + dict({ + 'domain': 'group', + 'services': dict({ + 'reload': dict({ + 'description': 'Reloads group configuration, entities, and notify services from YAML-configuration.', + 'fields': dict({ + }), + 'name': 'Reload', + }), + 'remove': dict({ + 'description': 'Removes a group.', + 'fields': dict({ + 'object_id': dict({ + 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', + 'example': 'test_group', + 'name': 'Object ID', + 'required': True, + 'selector': dict({ + 'object': dict({ + }), + }), + }), + }), + 'name': 'Remove', + }), + 'set': dict({ + 'description': 'Creates/Updates a group.', + 'fields': dict({ + 'add_entities': dict({ + 'description': 'List of members to be added to the group. Cannot be used in combination with `Entities` or `Remove entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Add entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + 'all': dict({ + 'description': 'Enable this option if the group should only be used when all entities are in state `on`.', + 'name': 'All', + 'selector': dict({ + 'boolean': dict({ + }), + }), + }), + 'entities': dict({ + 'description': 'List of all members in the group. Cannot be used in combination with `Add entities` or `Remove entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + 'icon': dict({ + 'description': 'Name of the icon for the group.', + 'example': 'mdi:camera', + 'name': 'Icon', + 'selector': dict({ + 'icon': dict({ + }), + }), + }), + 'name': dict({ + 'description': 'Name of the group.', + 'example': 'My test group', + 'name': 'Name', + 'selector': dict({ + 'text': dict({ + }), + }), + }), + 'object_id': dict({ + 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', + 'example': 'test_group', + 'name': 'Object ID', + 'required': True, + 'selector': dict({ + 'text': dict({ + }), + }), + }), + 'remove_entities': dict({ + 'description': 'List of members to be removed from a group. Cannot be used in combination with `Entities` or `Add entities`.', + 'example': 'domain.entity_id1, domain.entity_id2', + 'name': 'Remove entities', + 'selector': dict({ + 'entity': dict({ + 'multiple': True, + 'reorder': False, + }), + }), + }), + }), + 'name': 'Set', + }), + }), + }), + ]) +# --- +# name: test_api_get_services.1 + dict({ + 'domain': 'logger', + 'services': dict({ + 'set_default_level': dict({ + 'description': 'Translated description', + 'fields': dict({ + 'level': dict({ + 'description': 'Field description', + 'example': 'Field example', + 'name': 'Field name', + 'selector': dict({ + 'select': dict({ + 'options': list([ + 'debug', + 'info', + 'warning', + 'error', + 'fatal', + 'critical', + ]), + 'translation_key': 'level', + }), + }), + }), + }), + 'name': 'Translated name', + }), + 'set_level': dict({ + 'description': '', + 'fields': dict({ + }), + 'name': '', + }), + }), + }) +# --- diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index bc484a1632a..382b88b89ea 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -4,18 +4,24 @@ import asyncio from http import HTTPStatus import json from typing import Any -from unittest.mock import patch +from unittest.mock import ANY, patch from aiohttp import ServerDisconnectedError, web from aiohttp.test_utils import TestClient import pytest +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant import const, core as ha from homeassistant.auth.models import Credentials from homeassistant.bootstrap import DATA_LOGGING +from homeassistant.components.group import DOMAIN as DOMAIN_GROUP +from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER +from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH from homeassistant.core import HomeAssistant +from homeassistant.loader import Integration from homeassistant.setup import async_setup_component +from homeassistant.util.yaml.loader import JSON_TYPE from tests.common import CLIENT_ID, MockUser, async_mock_service from tests.typing import ClientSessionGenerator @@ -315,17 +321,72 @@ async def test_api_get_event_listeners( async def test_api_get_services( - hass: HomeAssistant, mock_api_client: TestClient + hass: HomeAssistant, + mock_api_client: TestClient, + snapshot: SnapshotAssertion, ) -> None: """Test if we can get a dict describing current services.""" + # Set up an integration that has services + assert await async_setup_component(hass, DOMAIN_GROUP, {DOMAIN_GROUP: {}}) + + # Set up an integration that has no services + assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {}) + resp = await mock_api_client.get(const.URL_API_SERVICES) data = await resp.json() - local_services = hass.services.async_services() - for serv_domain in data: - local = local_services.pop(serv_domain["domain"]) + assert data == snapshot - assert serv_domain["services"].keys() == local.keys() + # Set up an integration with legacy translations in services.yaml + def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE: + return { + "set_default_level": { + "description": "Translated description", + "fields": { + "level": { + "description": "Field description", + "example": "Field example", + "name": "Field name", + "selector": { + "select": { + "options": [ + "debug", + "info", + "warning", + "error", + "fatal", + "critical", + ], + "translation_key": "level", + } + }, + } + }, + "name": "Translated name", + }, + "set_level": None, + } + + await async_setup_component(hass, DOMAIN_LOGGER, {DOMAIN_LOGGER: {}}) + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.helpers.service._load_services_file", + side_effect=_load_services_file, + ), + patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={}, + ), + ): + resp = await mock_api_client.get(const.URL_API_SERVICES) + + data2 = await resp.json() + + assert data2 == [*data, {"domain": DOMAIN_LOGGER, "services": ANY}] + + assert data2[-1] == snapshot async def test_api_call_service_no_data(