mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-05-19 14:18:53 +01:00
7fb621234e
* Use Unix socket for Supervisor to Core communication Reintroduce Unix socket support for Supervisor-to-Core communication (reverted in #6735) with the addition of a feature flag gate. The feature is now controlled by the `core_unix_socket` feature flag and disabled by default. When enabled and Core version supports it, Supervisor communicates with Core via a Unix socket at /run/os/core.sock instead of TCP. This eliminates the need for access token authentication on the socket path, as Core authenticates the peer by the socket connection itself. Key changes: - Add FeatureFlag.CORE_UNIX_SOCKET to gate the feature - HomeAssistantAPI: transport-aware session/url/websocket management - WSClient: separate connect() (Unix, no auth) and connect_with_auth() (TCP) class methods with proper error handling - APIProxy delegates websocket setup to api.connect_websocket() - Container state tracking for Unix session lifecycle - CI builder mounts /run/supervisor for integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Sort feature flags alphabetically * Drop per-call max_msg_size from WSClient Hardcode the WebSocket message size cap to 64 MB in WSClient and remove the parameter from WSClient.connect, connect_with_auth, _ws_connect, and HomeAssistantAPI.connect_websocket. This was only ever overridden by APIProxy, so threading it through four layers was unnecessary. max_msg_size is a cap, not a pre-allocation; aiohttp only grows buffers to the size of actual incoming messages. Supervisor's own control channel never approaches 64 MB, so unifying the limit has no runtime cost. Addresses review feedback on #6742. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
"""Test discovery API."""
|
|
|
|
import logging
|
|
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
|
|
|
from aiohttp.test_utils import TestClient
|
|
import pytest
|
|
|
|
from supervisor.addons.addon import App
|
|
from supervisor.const import AppState
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.discovery import Message
|
|
|
|
from tests.common import load_json_fixture
|
|
from tests.const import TEST_ADDON_SLUG
|
|
|
|
|
|
@pytest.mark.parametrize("api_client", ["local_ssh"], indirect=True)
|
|
async def test_api_discovery_forbidden(
|
|
api_client: TestClient, caplog: pytest.LogCaptureFixture, install_app_ssh
|
|
):
|
|
"""Test app sending discovery message for an unregistered service."""
|
|
caplog.clear()
|
|
|
|
with caplog.at_level(logging.ERROR):
|
|
resp = await api_client.post(
|
|
"/discovery", json={"service": "mqtt", "config": {}}
|
|
)
|
|
|
|
assert resp.status == 403
|
|
result = await resp.json()
|
|
assert result["result"] == "error"
|
|
assert (
|
|
result["message"]
|
|
== "Apps must list services they provide via discovery in their config!"
|
|
)
|
|
assert "Please report this to the maintainer of the app" in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"skip_state", [AppState.ERROR, AppState.STOPPED, AppState.STARTUP]
|
|
)
|
|
async def test_api_list_discovery(
|
|
api_client: TestClient,
|
|
coresys: CoreSys,
|
|
install_app_ssh: App,
|
|
skip_state: AppState,
|
|
):
|
|
"""Test listing discovery messages only returns ones for healthy services."""
|
|
with (
|
|
patch(
|
|
"supervisor.utils.common.read_json_or_yaml_file",
|
|
return_value=load_json_fixture("discovery.json"),
|
|
),
|
|
patch("supervisor.utils.common.Path.is_file", return_value=True),
|
|
):
|
|
await coresys.discovery.read_data()
|
|
|
|
await coresys.discovery.load()
|
|
assert coresys.discovery.list_messages == [
|
|
Message(addon="core_mosquitto", service="mqtt", config=ANY, uuid=ANY),
|
|
Message(addon="local_ssh", service="adguard", config=ANY, uuid=ANY),
|
|
]
|
|
|
|
install_app_ssh.state = AppState.STARTED
|
|
resp = await api_client.get("/discovery")
|
|
assert resp.status == 200
|
|
result = await resp.json()
|
|
assert result["data"]["discovery"] == [
|
|
{
|
|
"addon": "local_ssh",
|
|
"service": "adguard",
|
|
"config": ANY,
|
|
"uuid": ANY,
|
|
}
|
|
]
|
|
|
|
install_app_ssh.state = skip_state
|
|
resp = await api_client.get("/discovery")
|
|
assert resp.status == 200
|
|
result = await resp.json()
|
|
assert result["data"]["discovery"] == []
|
|
|
|
|
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
|
async def test_api_send_del_discovery(
|
|
api_client: TestClient,
|
|
coresys: CoreSys,
|
|
install_app_ssh: App,
|
|
websession: MagicMock,
|
|
):
|
|
"""Test adding and removing discovery."""
|
|
install_app_ssh.data["discovery"] = ["test"]
|
|
coresys.homeassistant.api._ensure_access_token = AsyncMock() # pylint: disable=protected-access
|
|
|
|
resp = await api_client.post("/discovery", json={"service": "test", "config": {}})
|
|
assert resp.status == 200
|
|
result = await resp.json()
|
|
uuid = result["data"]["uuid"]
|
|
coresys.websession.request.assert_called_once()
|
|
assert coresys.websession.request.call_args.args[0] == "post"
|
|
assert (
|
|
coresys.websession.request.call_args.args[1]
|
|
== f"http://172.30.32.1:8123/api/hassio_push/discovery/{uuid}"
|
|
)
|
|
assert coresys.websession.request.call_args.kwargs["json"] == {
|
|
"addon": TEST_ADDON_SLUG,
|
|
"service": "test",
|
|
"uuid": uuid,
|
|
}
|
|
|
|
message = coresys.discovery.get(uuid)
|
|
assert message.addon == TEST_ADDON_SLUG
|
|
assert message.service == "test"
|
|
assert message.config == {}
|
|
|
|
coresys.websession.request.reset_mock()
|
|
resp = await api_client.delete(f"/discovery/{uuid}")
|
|
assert resp.status == 200
|
|
coresys.websession.request.assert_called_once()
|
|
assert coresys.websession.request.call_args.args[0] == "delete"
|
|
assert (
|
|
coresys.websession.request.call_args.args[1]
|
|
== f"http://172.30.32.1:8123/api/hassio_push/discovery/{uuid}"
|
|
)
|
|
assert coresys.websession.request.call_args.kwargs["json"] == {
|
|
"addon": TEST_ADDON_SLUG,
|
|
"service": "test",
|
|
"uuid": uuid,
|
|
}
|
|
|
|
assert coresys.discovery.get(uuid) is None
|
|
|
|
|
|
@pytest.mark.parametrize("api_client", [TEST_ADDON_SLUG], indirect=True)
|
|
async def test_api_invalid_discovery(api_client: TestClient, install_app_ssh: App):
|
|
"""Test invalid discovery messages."""
|
|
install_app_ssh.data["discovery"] = ["test"]
|
|
|
|
resp = await api_client.post("/discovery", json={"service": "test"})
|
|
assert resp.status == 400
|
|
|
|
resp = await api_client.post("/discovery", json={"service": "test", "config": None})
|
|
assert resp.status == 400
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("method", "url"),
|
|
[("get", "/discovery/bad"), ("delete", "/discovery/bad")],
|
|
)
|
|
async def test_discovery_not_found(api_client: TestClient, method: str, url: str):
|
|
"""Test discovery not found error."""
|
|
resp = await api_client.request(method, url)
|
|
assert resp.status == 404
|
|
resp = await resp.json()
|
|
assert resp["message"] == "Discovery message not found"
|