From 774dfa429d9ebc348096cfe22362f918d24b5fd4 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 27 Apr 2026 18:25:35 +0000 Subject: [PATCH 1/7] Require admin for supervisor event publishing and addon options info --- .../components/hassio/websocket_api.py | 16 ++++++- tests/components/hassio/test_websocket_api.py | 44 ++++++++++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 21b8dbf8e124fd..e5e8d971d930f4 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -49,10 +49,12 @@ # Endpoints needed for ingress can't require admin because addons can set `panel_admin: false` # fmt: off +RE_ADDONS_INFO_ENDPOINT = r"^/addons/[^/]+/info$" +WS_ADDONS_INFO_ENDPOINT = re.compile(RE_ADDONS_INFO_ENDPOINT) WS_NO_ADMIN_ENDPOINTS = re.compile( r"^(?:" r"|/ingress/(session|validate_session)" - r"|/addons/[^/]+/info" + f"|{RE_ADDONS_INFO_ENDPOINT}" r")$" ) # fmt: on @@ -92,6 +94,7 @@ def forward_messages(data: dict[str, str]) -> None: @callback +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_EVENT, @@ -150,7 +153,16 @@ async def websocket_supervisor_api( msg[WS_ID], code=websocket_api.ERR_UNKNOWN_ERROR, message=str(err) ) else: - connection.send_result(msg[WS_ID], result.get(ATTR_DATA, {})) + data = result.get(ATTR_DATA, {}) + # Remove options from addon info if user is not admin, as options can contain + # sensitive information and the frontend does not require it for ingress. + if ( + not connection.user.is_admin + and WS_ADDONS_INFO_ENDPOINT.match(command) + and "options" in data + ): + data["options"] = {} + connection.send_result(msg[WS_ID], data) @websocket_api.require_admin diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 89090edd28b067..c61c859f633ba4 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -131,6 +131,27 @@ async def test_ws_subscription( assert response["success"] +@pytest.mark.usefixtures("hassio_env") +async def test_non_admin_publish_supervisor_event_failure( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_admin_user: MockUser +) -> None: + """Test non admin user cannot publish supervisor event.""" + hass_admin_user.groups = [] + assert await async_setup_component(hass, "hassio", {}) + client = await hass_ws_client(hass) + + await client.send_json( + { + WS_ID: 1, + WS_TYPE: "supervisor/event", + ATTR_DATA: {ATTR_WS_EVENT: "test", "lorem": "ipsum"}, + } + ) + msg = await client.receive_json() + assert msg["success"] is False + assert msg["error"]["message"] == "Unauthorized" + + @pytest.mark.usefixtures("hassio_env") async def test_websocket_supervisor_api( hass: HomeAssistant, @@ -277,7 +298,17 @@ async def test_websocket_non_admin_user( websocket_client = await hass_ws_client(hass) aioclient_mock.get( "http://127.0.0.1/addons/test_addon/info", - json={"result": "ok", "data": {}}, + json={ + "result": "ok", + "data": { + "name": "test", + "state": "started", + "slug": "test_addon", + "version": "2.0.0", + "ingress_url": "http://127.0.0.1/ingress/test_addon", + "options": {"option1": "value1", "option2": "value2"}, + }, + }, ) aioclient_mock.get( "http://127.0.0.1/ingress/session", @@ -288,6 +319,8 @@ async def test_websocket_non_admin_user( json={"result": "ok", "data": {}}, ) + # Should return the fields frontend needs (name, version, state, slug and ingress_url) + # but not options, as user is not admin and options can contain sensitive information await websocket_client.send_json( { WS_ID: 1, @@ -297,7 +330,14 @@ async def test_websocket_non_admin_user( } ) msg = await websocket_client.receive_json() - assert msg["result"] == {} + assert msg["result"] == { + "name": "test", + "state": "started", + "slug": "test_addon", + "version": "2.0.0", + "ingress_url": "http://127.0.0.1/ingress/test_addon", + "options": {}, + } await websocket_client.send_json( { From dd4c2e1626fa99abb3b145d82dec85dee9f4dda6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:09:01 +0000 Subject: [PATCH 2/7] Address review: fix regex double-anchoring, remove options key for non-admin users Agent-Logs-Url: https://github.com/home-assistant/core/sessions/a4f8418d-42e2-4bb4-bbb3-b499007c6a64 Co-authored-by: balloob <1444314+balloob@users.noreply.github.com> --- homeassistant/components/hassio/websocket_api.py | 11 ++++------- tests/components/hassio/test_websocket_api.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index e5e8d971d930f4..0c38568156ebe4 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -48,16 +48,14 @@ ) # Endpoints needed for ingress can't require admin because addons can set `panel_admin: false` -# fmt: off -RE_ADDONS_INFO_ENDPOINT = r"^/addons/[^/]+/info$" -WS_ADDONS_INFO_ENDPOINT = re.compile(RE_ADDONS_INFO_ENDPOINT) +RE_ADDONS_INFO_ENDPOINT = r"/addons/[^/]+/info" +WS_ADDONS_INFO_ENDPOINT = re.compile(r"^" + RE_ADDONS_INFO_ENDPOINT + r"$") WS_NO_ADMIN_ENDPOINTS = re.compile( r"^(?:" r"|/ingress/(session|validate_session)" f"|{RE_ADDONS_INFO_ENDPOINT}" r")$" ) -# fmt: on _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -154,14 +152,13 @@ async def websocket_supervisor_api( ) else: data = result.get(ATTR_DATA, {}) - # Remove options from addon info if user is not admin, as options can contain + # Remove options from addon info for non-admin users, as options can contain # sensitive information and the frontend does not require it for ingress. if ( not connection.user.is_admin and WS_ADDONS_INFO_ENDPOINT.match(command) - and "options" in data ): - data["options"] = {} + data.pop("options", None) connection.send_result(msg[WS_ID], data) diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index c61c859f633ba4..086a9b75f34ccf 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -336,8 +336,8 @@ async def test_websocket_non_admin_user( "slug": "test_addon", "version": "2.0.0", "ingress_url": "http://127.0.0.1/ingress/test_addon", - "options": {}, } + assert "options" not in msg["result"] await websocket_client.send_json( { From 88f2d9c193f7658884f6330077738e314efa933c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:09:59 +0000 Subject: [PATCH 3/7] Use "add-on" spelling in comments to match HA standard terminology Agent-Logs-Url: https://github.com/home-assistant/core/sessions/a4f8418d-42e2-4bb4-bbb3-b499007c6a64 Co-authored-by: balloob <1444314+balloob@users.noreply.github.com> --- homeassistant/components/hassio/websocket_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 0c38568156ebe4..582b567dc4a9b2 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -47,7 +47,7 @@ extra=vol.ALLOW_EXTRA, ) -# Endpoints needed for ingress can't require admin because addons can set `panel_admin: false` +# Endpoints needed for ingress can't require admin because add-ons can set `panel_admin: false` RE_ADDONS_INFO_ENDPOINT = r"/addons/[^/]+/info" WS_ADDONS_INFO_ENDPOINT = re.compile(r"^" + RE_ADDONS_INFO_ENDPOINT + r"$") WS_NO_ADMIN_ENDPOINTS = re.compile( @@ -152,7 +152,7 @@ async def websocket_supervisor_api( ) else: data = result.get(ATTR_DATA, {}) - # Remove options from addon info for non-admin users, as options can contain + # Remove options from add-on info for non-admin users, as options can contain # sensitive information and the frontend does not require it for ingress. if ( not connection.user.is_admin From 6aa3c66c5ffb5318ff695207c333238c36f526f4 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 27 Apr 2026 19:28:43 +0000 Subject: [PATCH 4/7] Ruff format fix --- homeassistant/components/hassio/websocket_api.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 582b567dc4a9b2..286b9922f6ea80 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -154,10 +154,7 @@ async def websocket_supervisor_api( data = result.get(ATTR_DATA, {}) # Remove options from add-on info for non-admin users, as options can contain # sensitive information and the frontend does not require it for ingress. - if ( - not connection.user.is_admin - and WS_ADDONS_INFO_ENDPOINT.match(command) - ): + if not connection.user.is_admin and WS_ADDONS_INFO_ENDPOINT.match(command): data.pop("options", None) connection.send_result(msg[WS_ID], data) From 70f6eea9004a96fc8078e57b0f44ff52df39ff3b Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 28 Apr 2026 09:07:12 -0400 Subject: [PATCH 5/7] Only supervisor can provide supervisor events Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Stefan Agner --- homeassistant/components/hassio/websocket_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 286b9922f6ea80..4362eca19859b1 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -52,7 +52,7 @@ WS_ADDONS_INFO_ENDPOINT = re.compile(r"^" + RE_ADDONS_INFO_ENDPOINT + r"$") WS_NO_ADMIN_ENDPOINTS = re.compile( r"^(?:" - r"|/ingress/(session|validate_session)" + r"/ingress/(session|validate_session)" f"|{RE_ADDONS_INFO_ENDPOINT}" r")$" ) @@ -92,7 +92,7 @@ def forward_messages(data: dict[str, str]) -> None: @callback -@websocket_api.require_admin +@websocket_api.ws_require_user(only_supervisor=True) @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_EVENT, From 6252d52072e417af9f576c8b77afc73289596d48 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 28 Apr 2026 17:26:58 +0000 Subject: [PATCH 6/7] Fix tests after requiring supervisor for supervisor events Update tests that send supervisor/event WebSocket messages to use a new hass_supervisor_ws_client fixture that authenticates as the Supervisor user, since the endpoint now requires only_supervisor=True instead of require_admin. Also update test_non_admin_publish_supervisor_event_failure to expect the new error message "Only allowed as Supervisor". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/components/hassio/conftest.py | 21 ++++++- tests/components/hassio/test_backup.py | 56 +++++++++---------- tests/components/hassio/test_binary_sensor.py | 4 +- tests/components/hassio/test_issues.py | 52 ++++++++--------- tests/components/hassio/test_jobs.py | 12 ++-- tests/components/hassio/test_update.py | 28 +++++----- tests/components/hassio/test_websocket_api.py | 6 +- 7 files changed, 101 insertions(+), 78 deletions(-) diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index a3a28df61b2f42..7ef4d32cd60ed9 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -17,7 +17,7 @@ from . import SUPERVISOR_TOKEN -from tests.typing import ClientSessionGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture(autouse=True) @@ -65,6 +65,25 @@ async def hassio_client_supervisor( ) +@pytest.fixture +def hass_supervisor_ws_client( + hass_ws_client: WebSocketGenerator, + hass: HomeAssistant, +) -> WebSocketGenerator: + """Return a websocket client authenticated as the Supervisor user.""" + + async def create_client() -> WebSocketGenerator: + hassio_user_id = hass.data[DATA_CONFIG_STORE].data.hassio_user + hassio_user = await hass.auth.async_get_user(hassio_user_id) + assert hassio_user + assert hassio_user.refresh_tokens + refresh_token = next(iter(hassio_user.refresh_tokens.values())) + access_token = hass.auth.async_create_access_token(refresh_token) + return await hass_ws_client(hass, access_token=access_token) + + return create_client + + @pytest.fixture async def hassio_handler(hass: HomeAssistant) -> AsyncGenerator[HassIO]: """Create mock hassio handler.""" diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index 34b8c76ccc9a69..81ef255e3a8edb 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -934,13 +934,13 @@ async def test_agent_delete_with_error( ) async def test_agents_notify_on_mount_added_removed( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, event_data: dict[str, Any], mount_info_calls: int, ) -> None: """Test the listener is called when mounts are added or removed.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() assert supervisor_client.mounts.info.call_count == 1 assert supervisor_client.mounts.info.call_args[0] == () supervisor_client.mounts.info.reset_mock() @@ -1019,14 +1019,14 @@ async def test_agents_notify_on_mount_added_removed( ) async def test_reader_writer_create( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, supervisor_client: AsyncMock, extra_generate_options: dict[str, Any], expected_supervisor_options: supervisor_backups.PartialBackupOptions, ) -> None: """Test generating a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() freezer.move_to("2025-01-30 13:42:12.345678") supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS @@ -1112,7 +1112,7 @@ async def test_reader_writer_create( ) async def test_reader_writer_create_addon_folder_error( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, supervisor_client: AsyncMock, addon_info_side_effect: list[Any], @@ -1121,7 +1121,7 @@ async def test_reader_writer_create_addon_folder_error( addon_info_side_effect[0].name = "Advanced SSH & Web Terminal" assert dt.datetime.__name__ == "HAFakeDatetime" assert dt.HAFakeDatetime.__name__ == "HAFakeDatetime" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() freezer.move_to("2025-01-30 13:42:12.345678") supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS @@ -1242,12 +1242,12 @@ async def test_reader_writer_create_addon_folder_error( @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_create_report_progress( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, supervisor_client: AsyncMock, ) -> None: """Test generating a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() freezer.move_to("2025-01-30 13:42:12.345678") supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS @@ -1573,7 +1573,7 @@ async def test_reader_writer_create_job_done( ) async def test_reader_writer_create_per_agent_encryption( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, supervisor_client: AsyncMock, commands: list[dict[str, Any]], @@ -1585,7 +1585,7 @@ async def test_reader_writer_create_per_agent_encryption( upload_locations: list[str | None], ) -> None: """Test generating a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() freezer.move_to("2025-01-30 13:42:12.345678") mounts = MountsInfo( default_backup_mount=None, @@ -1809,12 +1809,12 @@ async def test_reader_writer_create_partial_backup_error( @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_create_missing_reference_error( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, supervisor_event: dict[str, Any], ) -> None: """Test missing reference error when generating a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE @@ -1914,7 +1914,7 @@ async def test_reader_writer_create_missing_reference_error( ) async def test_reader_writer_create_download_remove_error( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, exception: Exception, method: str, @@ -1923,7 +1923,7 @@ async def test_reader_writer_create_download_remove_error( expected_events_before_failed: list[dict[str, str]], ) -> None: """Test download and remove error when generating a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS_5 supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE @@ -2011,12 +2011,12 @@ async def test_reader_writer_create_download_remove_error( @pytest.mark.parametrize("exception", [SupervisorError("Boom!"), Exception("Boom!")]) async def test_reader_writer_create_info_error( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, exception: Exception, ) -> None: """Test backup info error when generating a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.backup_info.side_effect = exception supervisor_client.jobs.get_job.return_value = TEST_JOB_NOT_DONE @@ -2087,12 +2087,12 @@ async def test_reader_writer_create_info_error( @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_create_remote_backup( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, supervisor_client: AsyncMock, ) -> None: """Test generating a backup which will be uploaded to a remote agent.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() freezer.move_to("2025-01-30 13:42:12.345678") supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS_5 @@ -2301,13 +2301,13 @@ async def test_agent_receive_remote_backup( @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_restore( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, get_job_result: supervisor_jobs.Job, supervisor_events: list[dict[str, Any]], ) -> None: """Test restoring a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() supervisor_client.backups.partial_restore.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.list.return_value = [TEST_BACKUP] supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS @@ -2368,11 +2368,11 @@ async def test_reader_writer_restore( @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_restore_remote_backup( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, ) -> None: """Test restoring a backup from a remote agent.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() supervisor_client.backups.partial_restore.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.list.return_value = [TEST_BACKUP_5] supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS_5 @@ -2465,11 +2465,11 @@ async def test_reader_writer_restore_remote_backup( @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_restore_report_progress( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, ) -> None: """Test restoring a backup.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() supervisor_client.backups.partial_restore.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.list.return_value = [TEST_BACKUP] supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS @@ -2646,11 +2646,11 @@ async def test_reader_writer_restore_error( @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_restore_late_error( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, ) -> None: """Test restoring a backup with error.""" - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() supervisor_client.backups.partial_restore.return_value.job_id = UUID(TEST_JOB_ID) supervisor_client.backups.list.return_value = [TEST_BACKUP] supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS @@ -2838,7 +2838,7 @@ async def test_restore_progress_after_restart( @pytest.mark.usefixtures("hassio_client") async def test_restore_progress_after_restart_report_progress( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, ) -> None: """Test restore backup progress after restart.""" @@ -2848,7 +2848,7 @@ async def test_restore_progress_after_restart_report_progress( with patch.dict(os.environ, MOCK_ENVIRON | {RESTORE_JOB_ID_ENV: TEST_JOB_ID}): assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}}) - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json_auto_id({"type": "backup/subscribe_events"}) response = await client.receive_json() diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index e0da6ae5923228..e87004c9b4d620 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -210,7 +210,7 @@ async def test_mount_refresh_after_issue( hass: HomeAssistant, entity_registry: er.EntityRegistry, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test hassio mount state is refreshed after an issue was send by the supervisor.""" # Add a mount. @@ -255,7 +255,7 @@ async def test_mount_refresh_after_issue( # Change mount state to failed, issue a repair, and verify entity's state. mock_mounts[0] = replace(mock_mounts[0], state=MountState.FAILED) - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() issue_uuid = uuid4().hex await client.send_json( { diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index f96f22e6d6ffa3..a680bb442b5a6d 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -247,7 +247,7 @@ async def test_unsupported_reasons( async def test_unhealthy_issues_add_remove( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test unhealthy issues added and removed from dispatches.""" mock_resolution_info(supervisor_client) @@ -255,7 +255,7 @@ async def test_unhealthy_issues_add_remove( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -304,7 +304,7 @@ async def test_unhealthy_issues_add_remove( async def test_unsupported_issues_add_remove( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test unsupported issues added and removed from dispatches.""" mock_resolution_info(supervisor_client) @@ -312,7 +312,7 @@ async def test_unsupported_issues_add_remove( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -361,7 +361,7 @@ async def test_unsupported_issues_add_remove( async def test_reset_issues_supervisor_restart( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """All issues reset on supervisor restart.""" mock_resolution_info( @@ -392,7 +392,7 @@ async def test_reset_issues_supervisor_restart( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() @@ -435,7 +435,7 @@ async def test_reset_issues_supervisor_restart( async def test_no_reset_issues_supervisor_update_found( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Issues do not reset because a supervisor update was found.""" mock_resolution_info( @@ -446,7 +446,7 @@ async def test_no_reset_issues_supervisor_update_found( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() @@ -479,7 +479,7 @@ async def test_no_reset_issues_supervisor_update_found( async def test_reasons_added_and_removed( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test an unsupported/unhealthy reasons being added and removed at same time.""" mock_resolution_info( @@ -491,7 +491,7 @@ async def test_reasons_added_and_removed( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() @@ -739,7 +739,7 @@ async def test_supervisor_issues_initial_failure( async def test_supervisor_issues_add_remove( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test supervisor issues added and removed from dispatches.""" mock_resolution_info(supervisor_client) @@ -747,7 +747,7 @@ async def test_supervisor_issues_add_remove( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -850,7 +850,7 @@ async def test_supervisor_issues_suggestions_fail( async def test_supervisor_remove_missing_issue_without_error( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test HA skips message to remove issue that it didn't know about (sync issue).""" mock_resolution_info(supervisor_client) @@ -858,7 +858,7 @@ async def test_supervisor_remove_missing_issue_without_error( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -902,7 +902,7 @@ async def test_system_is_not_ready( async def test_supervisor_issues_detached_addon_missing( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test supervisor issue for detached addon due to missing repository.""" mock_resolution_info(supervisor_client) @@ -910,7 +910,7 @@ async def test_supervisor_issues_detached_addon_missing( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -953,7 +953,7 @@ async def test_supervisor_issues_detached_addon_missing( async def test_supervisor_issues_ntp_sync_failed( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test supervisor issue for NTP sync failed.""" mock_resolution_info(supervisor_client) @@ -961,7 +961,7 @@ async def test_supervisor_issues_ntp_sync_failed( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -1008,7 +1008,7 @@ async def test_supervisor_issues_ntp_sync_failed( async def test_supervisor_issues_disk_lifetime( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test supervisor issue for disk lifetime nearly exceeded.""" mock_resolution_info(supervisor_client) @@ -1016,7 +1016,7 @@ async def test_supervisor_issues_disk_lifetime( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -1055,7 +1055,7 @@ async def test_supervisor_issues_disk_lifetime( async def test_supervisor_issues_free_space( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test supervisor issue for too little free space remaining.""" mock_resolution_info(supervisor_client) @@ -1063,7 +1063,7 @@ async def test_supervisor_issues_free_space( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -1106,7 +1106,7 @@ async def test_supervisor_issues_free_space( async def test_supervisor_issues_free_space_host_info_fail( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, host_info: AsyncMock, ) -> None: """Test supervisor issue for too little free space remaining without host info.""" @@ -1116,7 +1116,7 @@ async def test_supervisor_issues_free_space_host_info_fail( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { @@ -1162,7 +1162,7 @@ async def test_supervisor_issues_free_space_host_info_fail( async def test_supervisor_issues_addon_pwned( hass: HomeAssistant, supervisor_client: AsyncMock, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test supervisor issue for pwned secret in an addon.""" mock_resolution_info(supervisor_client) @@ -1170,7 +1170,7 @@ async def test_supervisor_issues_addon_pwned( result = await async_setup_component(hass, "hassio", {}) assert result - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { diff --git a/tests/components/hassio/test_jobs.py b/tests/components/hassio/test_jobs.py index 909b7ff571e5dc..70a302b44d69e4 100644 --- a/tests/components/hassio/test_jobs.py +++ b/tests/components/hassio/test_jobs.py @@ -89,7 +89,9 @@ async def test_disconnect_on_config_entry_reload( @pytest.mark.usefixtures("all_setup_requests") async def test_job_manager_ws_updates( - hass: HomeAssistant, jobs_info: AsyncMock, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + jobs_info: AsyncMock, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test job updates sync from Supervisor WS messages.""" result = await async_setup_component(hass, "hassio", {}) @@ -97,7 +99,7 @@ async def test_job_manager_ws_updates( jobs_info.assert_called_once() jobs_info.reset_mock() - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() data_coordinator: HassioMainDataUpdateCoordinator = hass.data[MAIN_COORDINATOR] assert not data_coordinator.jobs.current_jobs @@ -277,7 +279,9 @@ def mock_subcription_callback(job: Job) -> None: @pytest.mark.usefixtures("all_setup_requests") async def test_job_manager_reload_on_supervisor_restart( - hass: HomeAssistant, jobs_info: AsyncMock, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + jobs_info: AsyncMock, + hass_supervisor_ws_client: WebSocketGenerator, ) -> None: """Test job manager reloads cache on supervisor restart.""" jobs_info.return_value = JobsInfo( @@ -308,7 +312,7 @@ async def test_job_manager_reload_on_supervisor_restart( jobs_info.reset_mock() jobs_info.return_value = JobsInfo(ignore_conditions=[], jobs=[]) - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() # Make an example listener job_data: Job | None = None diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index fefce2d43e438b..48d9e9c7d49a1d 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -176,7 +176,7 @@ async def test_update_addon(hass: HomeAssistant, update_addon: AsyncMock) -> Non async def test_update_addon_progress( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_supervisor_ws_client: WebSocketGenerator ) -> None: """Test progress reporting for addon update.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) @@ -191,7 +191,7 @@ async def test_update_addon_progress( assert result await hass.async_block_till_done() - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() message_id = 0 job_uuid = uuid4().hex @@ -688,7 +688,7 @@ async def test_update_core(hass: HomeAssistant, supervisor_client: AsyncMock) -> async def test_update_core_progress( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_supervisor_ws_client: WebSocketGenerator ) -> None: """Test progress reporting for core update.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) @@ -703,7 +703,7 @@ async def test_update_core_progress( assert result await hass.async_block_till_done() - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() message_id = 0 job_uuid = uuid4().hex @@ -1102,7 +1102,7 @@ async def check_progress( async def test_update_addon_resets_progress_on_error( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, ) -> None: """Test addon update resets in_progress and update_percentage on failure.""" @@ -1122,7 +1122,7 @@ async def test_update_addon_resets_progress_on_error( assert state.attributes.get("in_progress") is False assert state.attributes.get("update_percentage") is None - ws = await hass_ws_client(hass) + ws = await hass_supervisor_ws_client() job_uuid = uuid4().hex async def fake_update_addon_error( @@ -1220,7 +1220,7 @@ def _updated_info(slug: str): async def test_update_addon_stays_in_progress_until_refresh( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, update_addon: AsyncMock, addon_installed: AsyncMock, addons_list: AsyncMock, @@ -1247,7 +1247,7 @@ async def test_update_addon_stays_in_progress_until_refresh( entity_id = "update.test_update" assert hass.states.get(entity_id).state == "on" - ws = await hass_ws_client(hass) + ws = await hass_supervisor_ws_client() job_uuid = uuid4().hex in_progress_after_done: list[bool | None] = [] @@ -1363,7 +1363,7 @@ async def test_update_supervisor( async def test_update_supervisor_progress( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_info: AsyncMock, ) -> None: """Test progress reporting for a Supervisor update that was not initiated via the entity. @@ -1384,7 +1384,7 @@ async def test_update_supervisor_progress( ) await hass.async_block_till_done() - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() message_id = 0 job_uuid = uuid4().hex entity_id = "update.home_assistant_supervisor_update" @@ -1470,7 +1470,7 @@ def make_job_message(progress: float, done: bool | None) -> dict[str, Any]: async def test_update_supervisor_stays_in_progress_until_restart( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, supervisor_info: AsyncMock, ) -> None: @@ -1505,7 +1505,7 @@ async def test_update_supervisor_stays_in_progress_until_restart( update_available=False, ) - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { "id": 1, @@ -1530,7 +1530,7 @@ async def test_update_supervisor_stays_in_progress_until_restart( async def test_update_supervisor_completes_on_any_version_change( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, + hass_supervisor_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, supervisor_info: AsyncMock, ) -> None: @@ -1566,7 +1566,7 @@ async def test_update_supervisor_completes_on_any_version_change( update_available=True, ) - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json( { "id": 1, diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 086a9b75f34ccf..61a8ae5cb2a942 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -95,11 +95,11 @@ def mock_all( @pytest.mark.usefixtures("hassio_env") async def test_ws_subscription( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_supervisor_ws_client: WebSocketGenerator ) -> None: """Test websocket subscription.""" assert await async_setup_component(hass, "hassio", {}) - client = await hass_ws_client(hass) + client = await hass_supervisor_ws_client() await client.send_json({WS_ID: 5, WS_TYPE: WS_TYPE_SUBSCRIBE}) response = await client.receive_json() assert response["success"] @@ -149,7 +149,7 @@ async def test_non_admin_publish_supervisor_event_failure( ) msg = await client.receive_json() assert msg["success"] is False - assert msg["error"]["message"] == "Unauthorized" + assert msg["error"]["message"] == "Only allowed as Supervisor" @pytest.mark.usefixtures("hassio_env") From 59f4a74fd9ea6aa50f1ee3a8e78e15919464cd4f Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 28 Apr 2026 14:22:51 -0400 Subject: [PATCH 7/7] Update tests/components/hassio/test_websocket_api.py Co-authored-by: Stefan Agner --- tests/components/hassio/test_websocket_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 61a8ae5cb2a942..ca58683f1b8769 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -132,7 +132,7 @@ async def test_ws_subscription( @pytest.mark.usefixtures("hassio_env") -async def test_non_admin_publish_supervisor_event_failure( +async def test_admin_non_supervisor_publish_supervisor_event_failure( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_admin_user: MockUser ) -> None: """Test non admin user cannot publish supervisor event."""