Files

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