mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-06-17 15:36:29 +00:00

When an error occurs when streaming Supervisor logs, the fallback method receives the follow kwarg as well, which is invalid for the Docker log handler: TypeError: APISupervisor.logs() got an unexpected keyword argument 'follow' The exception is still printed to the logs but with all the extra noise caused by this error. Removing the argument makes the stack trace more comprehensible and the fallback actually works as desired.
236 lines
8.3 KiB
Python
236 lines
8.3 KiB
Python
"""Test Supervisor API."""
|
|
|
|
# pylint: disable=protected-access
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from aiohttp.test_utils import TestClient
|
|
import pytest
|
|
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.exceptions import (
|
|
HassioError,
|
|
HostNotSupportedError,
|
|
StoreGitError,
|
|
StoreNotFound,
|
|
)
|
|
from supervisor.store.repository import Repository
|
|
|
|
from tests.api import common_test_api_advanced_logs
|
|
from tests.dbus_service_mocks.base import DBusServiceMock
|
|
from tests.dbus_service_mocks.os_agent import OSAgent as OSAgentService
|
|
|
|
REPO_URL = "https://github.com/awesome-developer/awesome-repo"
|
|
|
|
|
|
async def test_api_supervisor_options_debug(api_client: TestClient, coresys: CoreSys):
|
|
"""Test security options force security."""
|
|
assert not coresys.config.debug
|
|
|
|
await api_client.post("/supervisor/options", json={"debug": True})
|
|
|
|
assert coresys.config.debug
|
|
|
|
|
|
async def test_api_supervisor_options_add_repository(
|
|
api_client: TestClient, coresys: CoreSys
|
|
):
|
|
"""Test add a repository via POST /supervisor/options REST API."""
|
|
assert REPO_URL not in coresys.store.repository_urls
|
|
with pytest.raises(StoreNotFound):
|
|
coresys.store.get_from_url(REPO_URL)
|
|
|
|
with (
|
|
patch("supervisor.store.repository.Repository.load", return_value=None),
|
|
patch("supervisor.store.repository.Repository.validate", return_value=True),
|
|
):
|
|
response = await api_client.post(
|
|
"/supervisor/options", json={"addons_repositories": [REPO_URL]}
|
|
)
|
|
|
|
assert response.status == 200
|
|
assert REPO_URL in coresys.store.repository_urls
|
|
assert isinstance(coresys.store.get_from_url(REPO_URL), Repository)
|
|
|
|
|
|
async def test_api_supervisor_options_remove_repository(
|
|
api_client: TestClient, coresys: CoreSys, repository: Repository
|
|
):
|
|
"""Test remove a repository via POST /supervisor/options REST API."""
|
|
assert repository.source in coresys.store.repository_urls
|
|
assert repository.slug in coresys.store.repositories
|
|
|
|
response = await api_client.post(
|
|
"/supervisor/options", json={"addons_repositories": []}
|
|
)
|
|
|
|
assert response.status == 200
|
|
assert repository.source not in coresys.store.repository_urls
|
|
assert repository.slug not in coresys.store.repositories
|
|
|
|
|
|
@pytest.mark.parametrize("git_error", [None, StoreGitError()])
|
|
async def test_api_supervisor_options_repositories_skipped_on_error(
|
|
api_client: TestClient, coresys: CoreSys, git_error: StoreGitError
|
|
):
|
|
"""Test repositories skipped on error via POST /supervisor/options REST API."""
|
|
with (
|
|
patch("supervisor.store.repository.Repository.load", side_effect=git_error),
|
|
patch("supervisor.store.repository.Repository.validate", return_value=False),
|
|
patch("supervisor.store.repository.Repository.remove"),
|
|
):
|
|
response = await api_client.post(
|
|
"/supervisor/options", json={"addons_repositories": [REPO_URL]}
|
|
)
|
|
|
|
assert response.status == 400
|
|
assert len(coresys.resolution.suggestions) == 0
|
|
assert REPO_URL not in coresys.store.repository_urls
|
|
with pytest.raises(StoreNotFound):
|
|
coresys.store.get_from_url(REPO_URL)
|
|
|
|
|
|
async def test_api_supervisor_options_repo_error_with_config_change(
|
|
api_client: TestClient, coresys: CoreSys
|
|
):
|
|
"""Test config change with add repository error via POST /supervisor/options REST API."""
|
|
assert not coresys.config.debug
|
|
|
|
with patch(
|
|
"supervisor.store.repository.Repository.load", side_effect=StoreGitError()
|
|
):
|
|
response = await api_client.post(
|
|
"/supervisor/options",
|
|
json={"debug": True, "addons_repositories": [REPO_URL]},
|
|
)
|
|
|
|
assert response.status == 400
|
|
assert REPO_URL not in coresys.store.repository_urls
|
|
|
|
assert coresys.config.debug
|
|
coresys.updater.save_data.assert_called_once()
|
|
coresys.config.save_data.assert_called_once()
|
|
|
|
|
|
async def test_api_supervisor_options_auto_update(
|
|
api_client: TestClient, coresys: CoreSys
|
|
):
|
|
"""Test disabling auto update via api."""
|
|
assert coresys.updater.auto_update is True
|
|
|
|
response = await api_client.post("/supervisor/options", json={"auto_update": False})
|
|
|
|
assert response.status == 200
|
|
|
|
assert coresys.updater.auto_update is False
|
|
|
|
|
|
async def test_api_supervisor_options_diagnostics(
|
|
api_client: TestClient,
|
|
coresys: CoreSys,
|
|
os_agent_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
):
|
|
"""Test changing diagnostics."""
|
|
os_agent_service: OSAgentService = os_agent_services["os_agent"]
|
|
os_agent_service.Diagnostics = False
|
|
await os_agent_service.ping()
|
|
assert coresys.dbus.agent.diagnostics is False
|
|
|
|
with patch("supervisor.utils.sentry.sentry_sdk.init") as sentry_init:
|
|
response = await api_client.post(
|
|
"/supervisor/options", json={"diagnostics": True}
|
|
)
|
|
assert response.status == 200
|
|
sentry_init.assert_called_once()
|
|
|
|
await os_agent_service.ping()
|
|
assert coresys.dbus.agent.diagnostics is True
|
|
|
|
with patch("supervisor.api.supervisor.close_sentry") as close_sentry:
|
|
response = await api_client.post(
|
|
"/supervisor/options", json={"diagnostics": False}
|
|
)
|
|
assert response.status == 200
|
|
close_sentry.assert_called_once()
|
|
|
|
await os_agent_service.ping()
|
|
assert coresys.dbus.agent.diagnostics is False
|
|
|
|
|
|
async def test_api_supervisor_logs(api_client: TestClient, journald_logs: MagicMock):
|
|
"""Test supervisor logs."""
|
|
await common_test_api_advanced_logs(
|
|
"/supervisor", "hassio_supervisor", api_client, journald_logs
|
|
)
|
|
|
|
|
|
async def test_api_supervisor_fallback(
|
|
api_client: TestClient, journald_logs: MagicMock, docker_logs: MagicMock
|
|
):
|
|
"""Check that supervisor logs read from container logs if reading from journald gateway fails badly."""
|
|
journald_logs.side_effect = HassioError("Something bad happened!")
|
|
|
|
with patch("supervisor.api._LOGGER.exception") as logger:
|
|
resp = await api_client.get("/supervisor/logs")
|
|
logger.assert_called_once_with(
|
|
"Failed to get supervisor logs using advanced_logs API"
|
|
)
|
|
|
|
assert resp.status == 200
|
|
assert resp.content_type == "text/plain"
|
|
content = await resp.read()
|
|
assert content.split(b"\n")[0:2] == [
|
|
b"\x1b[36m22-10-11 14:04:23 DEBUG (MainThread) [supervisor.utils.dbus] D-Bus call - org.freedesktop.DBus.Properties.call_get_all on /io/hass/os\x1b[0m",
|
|
b"\x1b[36m22-10-11 14:04:23 DEBUG (MainThread) [supervisor.utils.dbus] D-Bus call - org.freedesktop.DBus.Properties.call_get_all on /io/hass/os/AppArmor\x1b[0m",
|
|
]
|
|
|
|
# check fallback also works for the follow endpoint (no mock reset needed)
|
|
|
|
with patch("supervisor.api._LOGGER.exception") as logger:
|
|
resp = await api_client.get("/supervisor/logs/follow")
|
|
logger.assert_called_once_with(
|
|
"Failed to get supervisor logs using advanced_logs API"
|
|
)
|
|
|
|
assert resp.status == 200
|
|
assert resp.content_type == "text/plain"
|
|
|
|
journald_logs.reset_mock()
|
|
|
|
# also check generic Python error
|
|
journald_logs.side_effect = OSError("Something bad happened!")
|
|
|
|
with patch("supervisor.api._LOGGER.exception") as logger:
|
|
resp = await api_client.get("/supervisor/logs")
|
|
logger.assert_called_once_with(
|
|
"Failed to get supervisor logs using advanced_logs API"
|
|
)
|
|
assert resp.status == 200
|
|
assert resp.content_type == "text/plain"
|
|
|
|
|
|
async def test_api_supervisor_fallback_log_capture(
|
|
api_client: TestClient, journald_logs: MagicMock, docker_logs: MagicMock
|
|
):
|
|
"""Check that Sentry log capture is executed only for unexpected errors."""
|
|
journald_logs.side_effect = HostNotSupportedError(
|
|
"No systemd-journal-gatewayd Unix socket available!"
|
|
)
|
|
|
|
with patch("supervisor.api.capture_exception") as capture_exception:
|
|
await api_client.get("/supervisor/logs")
|
|
capture_exception.assert_not_called()
|
|
|
|
journald_logs.reset_mock()
|
|
|
|
journald_logs.side_effect = HassioError("Something bad happened!")
|
|
|
|
with patch("supervisor.api.capture_exception") as capture_exception:
|
|
await api_client.get("/supervisor/logs")
|
|
capture_exception.assert_called_once()
|
|
|
|
|
|
async def test_api_supervisor_reload(api_client: TestClient):
|
|
"""Test supervisor reload."""
|
|
resp = await api_client.post("/supervisor/reload")
|
|
assert resp.status == 200
|