mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
362 lines
13 KiB
Python
362 lines
13 KiB
Python
"""Test add-on panel."""
|
|
|
|
from datetime import timedelta
|
|
from http import HTTPStatus
|
|
import os
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from aiohasupervisor import SupervisorError
|
|
from aiohasupervisor.models import IngressPanel
|
|
import pytest
|
|
|
|
from homeassistant.components.hassio import DOMAIN
|
|
from homeassistant.components.hassio.const import (
|
|
MAIN_COORDINATOR,
|
|
REQUEST_REFRESH_DELAY,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.setup import async_setup_component
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from tests.common import MockUser, async_fire_time_changed
|
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
|
|
|
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_all(all_setup_requests: None) -> None:
|
|
"""Mock all setup requests."""
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_client")
|
|
async def test_hassio_addon_panel_registered_on_setup(
|
|
hass: HomeAssistant, ingress_panels: AsyncMock
|
|
) -> None:
|
|
"""Test enabled panels are registered as part of config entry setup.
|
|
|
|
Regression test for https://github.com/home-assistant/supervisor/issues/7015:
|
|
registration must not depend on the one-shot EVENT_HOMEASSISTANT_START handler
|
|
that used to swallow Supervisor timeouts and never retry.
|
|
"""
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
"test2": IngressPanel(
|
|
enable=False, title="Test 2", icon="mdi:test2", admin=True
|
|
),
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.hassio.addon_panel._register_panel"
|
|
) as mock_panel,
|
|
patch.dict(os.environ, MOCK_ENVIRON),
|
|
):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
mock_panel.assert_called_once_with(
|
|
hass,
|
|
"test1",
|
|
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_client")
|
|
async def test_hassio_addon_panel_registration(
|
|
hass: HomeAssistant, ingress_panels: AsyncMock
|
|
) -> None:
|
|
"""Test panel registration calls frontend.async_register_built_in_panel."""
|
|
ingress_panels.return_value = {
|
|
"test_addon": IngressPanel(
|
|
enable=True, title="Test Addon", icon="mdi:test-tube", admin=True
|
|
),
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.hassio.addon_panel.frontend.async_register_built_in_panel"
|
|
) as mock_register,
|
|
patch.dict(os.environ, MOCK_ENVIRON),
|
|
):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
mock_register.assert_any_call(
|
|
hass,
|
|
"app",
|
|
frontend_url_path="test_addon",
|
|
sidebar_title="Test Addon",
|
|
sidebar_icon="mdi:test-tube",
|
|
require_admin=True,
|
|
config={"addon": "test_addon"},
|
|
update=True,
|
|
)
|
|
|
|
|
|
async def test_hassio_addon_panel_setup_retries_after_transient_error(
|
|
hass: HomeAssistant, ingress_panels: AsyncMock
|
|
) -> None:
|
|
"""Test a transient Supervisor error fetching panels causes setup to retry.
|
|
|
|
Regression test for https://github.com/home-assistant/supervisor/issues/7015:
|
|
previously a timeout fetching panels at startup was logged and swallowed,
|
|
leaving panels missing forever with no retry. Panel data is now fetched as
|
|
part of the main coordinator's first refresh, so a transient failure causes
|
|
the whole config entry setup to retry until Supervisor is reachable again.
|
|
"""
|
|
ingress_panels.side_effect = SupervisorError("Timeout connecting to Supervisor")
|
|
|
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
|
result = await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert result
|
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
|
|
|
ingress_panels.side_effect = None
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
}
|
|
|
|
with patch(
|
|
"homeassistant.components.hassio.addon_panel._register_panel"
|
|
) as mock_panel:
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert entry.state is ConfigEntryState.LOADED
|
|
mock_panel.assert_called_once_with(
|
|
hass,
|
|
"test1",
|
|
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
)
|
|
|
|
|
|
async def test_hassio_addon_panel_recovers_after_supervisor_restart(
|
|
hass: HomeAssistant,
|
|
hass_supervisor_ws_client: WebSocketGenerator,
|
|
ingress_panels: AsyncMock,
|
|
) -> None:
|
|
"""Test panels are refreshed when Supervisor reports it has restarted.
|
|
|
|
Regression test for the "Supervisor restarts while Core keeps running"
|
|
scenario: Supervisor fires a supervisor_update/startup:complete event on
|
|
every one of its own (re)starts, which the main coordinator already listens
|
|
for and uses to trigger a refresh.
|
|
"""
|
|
ingress_panels.return_value = {}
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.hassio.addon_panel._register_panel"
|
|
) as mock_panel,
|
|
patch.dict(os.environ, MOCK_ENVIRON),
|
|
):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
mock_panel.assert_not_called()
|
|
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(
|
|
enable=True, title="Test", icon="mdi:test", admin=False
|
|
),
|
|
}
|
|
|
|
client = await hass_supervisor_ws_client()
|
|
await client.send_json(
|
|
{
|
|
"id": 1,
|
|
"type": "supervisor/event",
|
|
"data": {
|
|
"event": "supervisor_update",
|
|
"update_key": "supervisor",
|
|
"data": {"startup": "complete"},
|
|
},
|
|
}
|
|
)
|
|
await client.receive_json()
|
|
|
|
async_fire_time_changed(
|
|
hass, dt_util.utcnow() + timedelta(seconds=REQUEST_REFRESH_DELAY + 1)
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_panel.assert_called_once_with(
|
|
hass,
|
|
"test1",
|
|
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_client")
|
|
async def test_hassio_addon_panel_api_post(
|
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
|
|
) -> None:
|
|
"""Test posting a panel push registers it via the coordinator cache."""
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
"test2": IngressPanel(
|
|
enable=False, title="Test 2", icon="mdi:test2", admin=True
|
|
),
|
|
}
|
|
|
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
hass_client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.hassio.addon_panel._register_panel"
|
|
) as mock_panel:
|
|
# Panel is not enabled yet according to Supervisor
|
|
resp = await hass_client.post("/api/hassio_push/panel/test2")
|
|
assert resp.status == HTTPStatus.BAD_REQUEST
|
|
mock_panel.assert_not_called()
|
|
|
|
# Supervisor enables the panel and pushes the change
|
|
ingress_panels.return_value["test2"] = IngressPanel(
|
|
enable=True, title="Test 2", icon="mdi:test2", admin=True
|
|
)
|
|
resp = await hass_client.post("/api/hassio_push/panel/test2")
|
|
assert resp.status == HTTPStatus.OK
|
|
mock_panel.assert_called_once_with(
|
|
hass,
|
|
"test2",
|
|
IngressPanel(enable=True, title="Test 2", icon="mdi:test2", admin=True),
|
|
)
|
|
|
|
# Posting again for an already-registered, unchanged panel is a no-op
|
|
mock_panel.reset_mock()
|
|
resp = await hass_client.post("/api/hassio_push/panel/test1")
|
|
assert resp.status == HTTPStatus.OK
|
|
mock_panel.assert_not_called()
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_client")
|
|
async def test_hassio_addon_panel_api_before_coordinator_ready(
|
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
|
|
) -> None:
|
|
"""Test panel push api falls back to a fresh Supervisor call before setup completes.
|
|
|
|
Other callers besides Supervisor may rely on this API before the config
|
|
entry (and its main coordinator) finishes setting up, so it must keep
|
|
working via a direct Supervisor call and frontend registration instead of
|
|
failing with a 503.
|
|
"""
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
}
|
|
|
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
hass_client = await hass_client()
|
|
|
|
# Simulate the main coordinator not being ready yet
|
|
del hass.data[MAIN_COORDINATOR]
|
|
|
|
with patch(
|
|
"homeassistant.components.hassio.addon_panel._register_panel"
|
|
) as mock_panel:
|
|
resp = await hass_client.post("/api/hassio_push/panel/test1")
|
|
assert resp.status == HTTPStatus.OK
|
|
mock_panel.assert_called_once_with(
|
|
hass,
|
|
"test1",
|
|
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.hassio.addon_panel.frontend.async_remove_panel"
|
|
) as mock_remove:
|
|
resp = await hass_client.delete("/api/hassio_push/panel/test1")
|
|
assert resp.status == HTTPStatus.OK
|
|
mock_remove.assert_called_once_with(hass, "test1", warn_if_unknown=False)
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_client")
|
|
async def test_hassio_addon_panel_api_non_admin(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
ingress_panels: AsyncMock,
|
|
hass_admin_user: MockUser,
|
|
) -> None:
|
|
"""Test register panel api fails with non admin user."""
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
}
|
|
|
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
hass_admin_user.groups = []
|
|
hass_client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.hassio.addon_panel._register_panel"
|
|
) as mock_panel:
|
|
# Both should return unauthorized regardless of enabled as the endpoint requires
|
|
# admin and the user is not admin
|
|
resp = await hass_client.post("/api/hassio_push/panel/test2")
|
|
assert resp.status == HTTPStatus.UNAUTHORIZED
|
|
|
|
resp = await hass_client.post("/api/hassio_push/panel/test1")
|
|
assert resp.status == HTTPStatus.UNAUTHORIZED
|
|
|
|
mock_panel.assert_not_called()
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_client")
|
|
async def test_hassio_addon_panel_api_delete(
|
|
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
|
|
) -> None:
|
|
"""Test panel api delete removes it via the coordinator cache."""
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
}
|
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
hass_client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.hassio.addon_panel.frontend.async_remove_panel"
|
|
) as mock_remove:
|
|
resp = await hass_client.delete("/api/hassio_push/panel/test1")
|
|
assert resp.status == HTTPStatus.OK
|
|
mock_remove.assert_called_once_with(hass, "test1", warn_if_unknown=False)
|
|
|
|
|
|
@pytest.mark.usefixtures("supervisor_client")
|
|
async def test_hassio_addon_panel_api_delete_non_admin(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
ingress_panels: AsyncMock,
|
|
hass_admin_user: MockUser,
|
|
) -> None:
|
|
"""Test panel api delete fails with non admin user."""
|
|
ingress_panels.return_value = {
|
|
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
|
}
|
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
|
await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
hass_admin_user.groups = []
|
|
hass_client = await hass_client()
|
|
|
|
with patch(
|
|
"homeassistant.components.hassio.addon_panel.frontend.async_remove_panel"
|
|
) as mock_remove:
|
|
resp = await hass_client.delete("/api/hassio_push/panel/test1")
|
|
assert resp.status == HTTPStatus.UNAUTHORIZED
|
|
mock_remove.assert_not_called()
|