1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-05-22 23:58:55 +01:00
Files
supervisor/tests/api/test_services.py
T
Stefan Agner 61ca2524b2 Return proper API errors for mqtt/mysql service conflicts (#6767)
* 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>
2026-04-24 21:56:12 +02:00

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"
)