mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-05-22 23:58:55 +01:00
61ca2524b2
* Return proper API errors for mqtt/mysql service conflicts After #6739 added unexpected-error logging and Sentry capture to the api_process wrappers, SUPERVISOR-1JTQ and SUPERVISOR-1JWM surfaced as user-triggered service conflicts that were being treated as unexpected errors: - POST /services/{mqtt,mysql} when another app already provides the service. - DELETE /services/{mqtt,mysql} when no app currently provides it. Both paths raised a generic ServicesError, which the API layer turned into an opaque HTTP 400 without a translation key, and which #6739 now also logs and captures via Sentry. Introduce ServiceAlreadyProvidedError (409 Conflict) and ServiceNotProvidedError (404 Not Found) as new-style API exceptions with translation keys and extra_fields, plus a shared APIConflict base class for future 409 responses. The mqtt and mysql service modules now raise these instead, so the API returns structured, translatable responses and these expected user conflicts stop being captured as bugs. Fixes SUPERVISOR-1JTQ Fixes SUPERVISOR-1JWM Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Don't log handled errors verbose Missing/already present service information are well handled errors with clear API responses. The client is supposed to handle these errors. No need to log verbosly. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""Test services API."""
|
|
|
|
from aiohttp.test_utils import TestClient
|
|
import pytest
|
|
|
|
from supervisor.addons.addon import App
|
|
from supervisor.const import ATTR_SERVICES
|
|
from supervisor.coresys import CoreSys
|
|
|
|
from tests.const import TEST_ADDON_SLUG
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("method", "url"),
|
|
[("get", "/services/bad"), ("post", "/services/bad"), ("delete", "/services/bad")],
|
|
)
|
|
async def test_service_not_found(api_client: TestClient, method: str, url: str):
|
|
"""Test service not found error."""
|
|
resp = await api_client.request(method, url)
|
|
assert resp.status == 404
|
|
body = await resp.json()
|
|
assert body["message"] == "Service does not exist"
|
|
|
|
|
|
@pytest.mark.parametrize("service", ["mqtt", "mysql"])
|
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
|
async def test_set_service_already_provided(
|
|
api_client: TestClient,
|
|
coresys: CoreSys,
|
|
install_app_ssh: App,
|
|
service: str,
|
|
):
|
|
"""Test setting service data when another app already provides it returns 409."""
|
|
install_app_ssh.data[ATTR_SERVICES] = [f"{service}:provide"]
|
|
await coresys.services.load()
|
|
|
|
coresys.services.data._data[service].update( # pylint: disable=protected-access
|
|
{"host": "existing", "port": 1883, "addon": "core_mosquitto"}
|
|
)
|
|
|
|
resp = await api_client.post(
|
|
f"/services/{service}",
|
|
json={"host": "new.example.com", "port": 1883},
|
|
)
|
|
assert resp.status == 409
|
|
body = await resp.json()
|
|
assert body["result"] == "error"
|
|
assert body["error_key"] == "service_already_provided_error"
|
|
assert body["extra_fields"] == {"service": service, "app": "core_mosquitto"}
|
|
assert (
|
|
body["message"]
|
|
== f"The {service} service is already provided by core_mosquitto"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("service", ["mqtt", "mysql"])
|
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
|
async def test_del_service_not_provided(
|
|
api_client: TestClient,
|
|
coresys: CoreSys,
|
|
install_app_ssh: App,
|
|
service: str,
|
|
):
|
|
"""Test deleting service data when no app provides it returns 404."""
|
|
install_app_ssh.data[ATTR_SERVICES] = [f"{service}:provide"]
|
|
await coresys.services.load()
|
|
|
|
coresys.services.data._data[service].clear() # pylint: disable=protected-access
|
|
|
|
resp = await api_client.delete(f"/services/{service}")
|
|
assert resp.status == 404
|
|
body = await resp.json()
|
|
assert body["result"] == "error"
|
|
assert body["error_key"] == "service_not_provided_error"
|
|
assert body["extra_fields"] == {"service": service}
|
|
assert (
|
|
body["message"] == f"The {service} service is not currently provided by any app"
|
|
)
|