Translate service validation errors (#115024)

* Move service validation error message to translation cache

* Fix test

* Revert unrelated change

* Address review comments

* Improve error message

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Jan Bouwhuis 2024-04-18 14:36:03 +02:00 committed by GitHub
parent d5e5a16303
commit 3299bc5ddc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 49 additions and 13 deletions

View File

@ -191,6 +191,18 @@
}, },
"service_not_found": { "service_not_found": {
"message": "Service {domain}.{service} not found." "message": "Service {domain}.{service} not found."
},
"service_does_not_supports_reponse": {
"message": "A service which does not return responses can't be called with {return_response}."
},
"service_lacks_response_request": {
"message": "The service call requires responses and must be called with {return_response}."
},
"service_reponse_invalid": {
"message": "Failed to process the returned service response data, expected a dictionary, but got {response_data_type}."
},
"service_should_be_blocking": {
"message": "A non blocking service call with argument {non_blocking_argument} can't be used together with argument {return_response}."
} }
} }
} }

View File

@ -86,6 +86,7 @@ from .exceptions import (
InvalidStateError, InvalidStateError,
MaxLengthExceeded, MaxLengthExceeded,
ServiceNotFound, ServiceNotFound,
ServiceValidationError,
Unauthorized, Unauthorized,
) )
from .helpers.deprecation import ( from .helpers.deprecation import (
@ -2571,16 +2572,27 @@ class ServiceRegistry:
if return_response: if return_response:
if not blocking: if not blocking:
raise ValueError( raise ServiceValidationError(
"Invalid argument return_response=True when blocking=False" translation_domain=DOMAIN,
translation_key="service_should_be_blocking",
translation_placeholders={
"return_response": "return_response=True",
"non_blocking_argument": "blocking=False",
},
) )
if handler.supports_response is SupportsResponse.NONE: if handler.supports_response is SupportsResponse.NONE:
raise ValueError( raise ServiceValidationError(
"Invalid argument return_response=True when handler does not support responses" translation_domain=DOMAIN,
translation_key="service_does_not_supports_reponse",
translation_placeholders={
"return_response": "return_response=True"
},
) )
elif handler.supports_response is SupportsResponse.ONLY: elif handler.supports_response is SupportsResponse.ONLY:
raise ValueError( raise ServiceValidationError(
"Service call requires responses but caller did not ask for responses" translation_domain=DOMAIN,
translation_key="service_lacks_response_request",
translation_placeholders={"return_response": "return_response=True"},
) )
if target: if target:
@ -2628,7 +2640,11 @@ class ServiceRegistry:
return None return None
if not isinstance(response_data, dict): if not isinstance(response_data, dict):
raise HomeAssistantError( raise HomeAssistantError(
f"Service response data expected a dictionary, was {type(response_data)}" translation_domain=DOMAIN,
translation_key="service_reponse_invalid",
translation_placeholders={
"response_data_type": str(type(response_data))
},
) )
return response_data return response_data

View File

@ -255,7 +255,7 @@ class UnknownUser(Unauthorized):
"""When call is made with user ID that doesn't exist.""" """When call is made with user ID that doesn't exist."""
class ServiceNotFound(HomeAssistantError): class ServiceNotFound(ServiceValidationError):
"""Raised when a service is not found.""" """Raised when a service is not found."""
def __init__(self, domain: str, service: str) -> None: def __init__(self, domain: str, service: str) -> None:

View File

@ -204,7 +204,7 @@ async def test_return_response_error(hass: HomeAssistant, websocket_client) -> N
assert msg["id"] == 8 assert msg["id"] == 8
assert msg["type"] == const.TYPE_RESULT assert msg["type"] == const.TYPE_RESULT
assert not msg["success"] assert not msg["success"]
assert msg["error"]["code"] == "unknown_error" assert msg["error"]["code"] == "service_validation_error"
@pytest.mark.parametrize("command", ["call_service", "call_service_action"]) @pytest.mark.parametrize("command", ["call_service", "call_service_action"])

View File

@ -55,6 +55,7 @@ from homeassistant.exceptions import (
InvalidStateError, InvalidStateError,
MaxLengthExceeded, MaxLengthExceeded,
ServiceNotFound, ServiceNotFound,
ServiceValidationError,
) )
from homeassistant.helpers.json import json_dumps from homeassistant.helpers.json import json_dumps
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -1791,8 +1792,9 @@ async def test_services_call_return_response_requires_blocking(
hass: HomeAssistant, hass: HomeAssistant,
) -> None: ) -> None:
"""Test that non-blocking service calls cannot ask for response data.""" """Test that non-blocking service calls cannot ask for response data."""
await async_setup_component(hass, "homeassistant", {})
async_mock_service(hass, "test_domain", "test_service") async_mock_service(hass, "test_domain", "test_service")
with pytest.raises(ValueError, match="when blocking=False"): with pytest.raises(ServiceValidationError, match="blocking=False") as exc:
await hass.services.async_call( await hass.services.async_call(
"test_domain", "test_domain",
"test_service", "test_service",
@ -1800,6 +1802,10 @@ async def test_services_call_return_response_requires_blocking(
blocking=False, blocking=False,
return_response=True, return_response=True,
) )
assert (
str(exc.value)
== "A non blocking service call with argument blocking=False can't be used together with argument return_response=True"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1816,6 +1822,7 @@ async def test_serviceregistry_return_response_invalid(
hass: HomeAssistant, response_data: Any, expected_error: str hass: HomeAssistant, response_data: Any, expected_error: str
) -> None: ) -> None:
"""Test service call response data must be json serializable objects.""" """Test service call response data must be json serializable objects."""
await async_setup_component(hass, "homeassistant", {})
def service_handler(call: ServiceCall) -> ServiceResponse: def service_handler(call: ServiceCall) -> ServiceResponse:
"""Service handler coroutine.""" """Service handler coroutine."""
@ -1842,8 +1849,8 @@ async def test_serviceregistry_return_response_invalid(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("supports_response", "return_response", "expected_error"), ("supports_response", "return_response", "expected_error"),
[ [
(SupportsResponse.NONE, True, "not support responses"), (SupportsResponse.NONE, True, "does not return responses"),
(SupportsResponse.ONLY, False, "caller did not ask for responses"), (SupportsResponse.ONLY, False, "call requires responses"),
], ],
) )
async def test_serviceregistry_return_response_arguments( async def test_serviceregistry_return_response_arguments(
@ -1853,6 +1860,7 @@ async def test_serviceregistry_return_response_arguments(
expected_error: str, expected_error: str,
) -> None: ) -> None:
"""Test service call response data invalid arguments.""" """Test service call response data invalid arguments."""
await async_setup_component(hass, "homeassistant", {})
hass.services.async_register( hass.services.async_register(
"test_domain", "test_domain",
@ -1861,7 +1869,7 @@ async def test_serviceregistry_return_response_arguments(
supports_response=supports_response, supports_response=supports_response,
) )
with pytest.raises(ValueError, match=expected_error): with pytest.raises(ServiceValidationError, match=expected_error):
await hass.services.async_call( await hass.services.async_call(
"test_domain", "test_domain",
"test_service", "test_service",