Files
supervisor/tests/test_updater.py
2025-10-28 15:48:00 +01:00

216 lines
8.0 KiB
Python

"""Test updater files."""
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, patch
from awesomeversion import AwesomeVersion
import pytest
from supervisor.const import ATTR_HASSOS_UNRESTRICTED, BusEvent
from supervisor.coresys import CoreSys
from supervisor.dbus.const import ConnectivityState
from supervisor.exceptions import UpdaterJobError
from supervisor.jobs import SupervisorJob
from supervisor.resolution.const import UnsupportedReason
from tests.common import MockResponse, load_binary_fixture
from tests.dbus_service_mocks.network_manager import (
NetworkManager as NetworkManagerService,
)
URL_TEST = "https://version.home-assistant.io/stable.json"
@pytest.mark.usefixtures("no_job_throttle")
async def test_fetch_versions(
coresys: CoreSys, mock_update_data: MockResponse, supervisor_internet: AsyncMock
) -> None:
"""Test download and sync version."""
coresys.security.force = True
await coresys.updater.fetch_data()
data = json.loads(await mock_update_data.text())
assert coresys.updater.version_supervisor == data["supervisor"]
assert coresys.updater.version_homeassistant == data["homeassistant"]["default"]
assert coresys.updater.version_audio == data["audio"]
assert coresys.updater.version_cli == data["cli"]
assert coresys.updater.version_dns == data["dns"]
assert coresys.updater.version_multicast == data["multicast"]
assert coresys.updater.version_observer == data["observer"]
assert coresys.updater.image_homeassistant == data["images"]["core"].format(
machine=coresys.machine
)
assert coresys.updater.image_supervisor == data["images"]["supervisor"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_cli == data["images"]["cli"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_audio == data["images"]["audio"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_dns == data["images"]["dns"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_observer == data["images"]["observer"].format(
arch=coresys.arch.supervisor
)
assert coresys.updater.image_multicast == data["images"]["multicast"].format(
arch=coresys.arch.supervisor
)
@pytest.mark.usefixtures("no_job_throttle")
@pytest.mark.parametrize(
"version, expected",
[
("3.1", "3.13"),
("4.5", "4.20"),
("5.0", "5.13"),
("6.4", "6.6"),
("4.20", "5.13"),
],
)
async def test_os_update_path(
coresys: CoreSys,
version: str,
expected: str,
mock_update_data: AsyncMock,
supervisor_internet: AsyncMock,
):
"""Test OS upgrade path across major versions."""
coresys.os._board = "rpi4" # pylint: disable=protected-access
coresys.os._version = AwesomeVersion(version) # pylint: disable=protected-access
await coresys.updater.fetch_data()
assert coresys.updater.version_hassos == AwesomeVersion(expected)
@pytest.mark.usefixtures("no_job_throttle")
async def test_delayed_fetch_for_connectivity(
coresys: CoreSys,
network_manager_service: NetworkManagerService,
websession: MagicMock,
):
"""Test initial version fetch waits for connectivity on load."""
coresys.websession.get = MagicMock()
coresys.websession.get.return_value.__aenter__.return_value.status = 200
coresys.websession.get.return_value.__aenter__.return_value.read.return_value = (
load_binary_fixture("version_stable.json")
)
coresys.websession.head = AsyncMock()
# Network connectivity change causes a series of async tasks to eventually do a version fetch
# Rather then use some kind of sleep loop, set up listener for start of fetch data job
event = asyncio.Event()
async def find_fetch_data_job_start(job: SupervisorJob):
if job.name == "updater_fetch_data":
event.set()
coresys.bus.register_event(BusEvent.SUPERVISOR_JOB_START, find_fetch_data_job_start)
# Start with no connectivity and confirm there is no version fetch on load
coresys.supervisor.connectivity = False
network_manager_service.connectivity = ConnectivityState.CONNECTIVITY_NONE.value
await coresys.host.network.load()
await coresys.host.network.check_connectivity()
await coresys.updater.load()
await coresys.updater.reload()
coresys.websession.get.assert_not_called()
# Now signal host has connectivity and wait for fetch data to complete to assert
network_manager_service.emit_properties_changed(
{"Connectivity": ConnectivityState.CONNECTIVITY_FULL}
)
await network_manager_service.ping()
async with asyncio.timeout(5):
await event.wait()
await asyncio.sleep(0)
coresys.websession.get.assert_called_once()
assert (
coresys.websession.get.call_args[0][0]
== "https://version.home-assistant.io/stable.json"
)
@pytest.mark.usefixtures("no_job_throttle")
async def test_load_calls_reload_when_os_board_without_version(
coresys: CoreSys, mock_update_data: MockResponse, supervisor_internet: AsyncMock
) -> None:
"""Test load calls reload when OS board exists but no version_hassos_unrestricted."""
# Set up OS board but no version data
coresys.os._board = "rpi4" # pylint: disable=protected-access
coresys.security.force = True
# Mock reload to verify it gets called
with patch.object(coresys.updater, "reload", new_callable=AsyncMock) as mock_reload:
await coresys.updater.load()
mock_reload.assert_called_once()
@pytest.mark.usefixtures("no_job_throttle")
async def test_load_skips_reload_when_os_board_with_version(
coresys: CoreSys, mock_update_data: MockResponse, supervisor_internet: AsyncMock
) -> None:
"""Test load skips reload when OS board exists and version_hassos_unrestricted is set."""
# Set up OS board and version data
coresys.os._board = "rpi4" # pylint: disable=protected-access
coresys.security.force = True
# Pre-populate version_hassos_unrestricted by setting it directly on the data dict
# Use the same approach as other tests that modify internal state
coresys.updater._data[ATTR_HASSOS_UNRESTRICTED] = AwesomeVersion("13.1") # pylint: disable=protected-access
# Mock reload to verify it doesn't get called
with patch.object(coresys.updater, "reload", new_callable=AsyncMock) as mock_reload:
await coresys.updater.load()
mock_reload.assert_not_called()
@pytest.mark.usefixtures("no_job_throttle")
async def test_load_skips_reload_when_no_os_board(
coresys: CoreSys, mock_update_data: MockResponse, supervisor_internet: AsyncMock
) -> None:
"""Test load skips reload when no OS board is set."""
# Ensure no OS board is set
coresys.os._board = None # pylint: disable=protected-access
# Mock reload to verify it doesn't get called
with patch.object(coresys.updater, "reload", new_callable=AsyncMock) as mock_reload:
await coresys.updater.load()
mock_reload.assert_not_called()
async def test_fetch_data_no_update_when_os_unsupported(
coresys: CoreSys, websession: MagicMock
) -> None:
"""Test that fetch_data doesn't update data when OS is unsupported."""
# Store initial versions to compare later
initial_supervisor_version = coresys.updater.version_supervisor
initial_homeassistant_version = coresys.updater.version_homeassistant
initial_hassos_version = coresys.updater.version_hassos
coresys.websession.head = AsyncMock()
# Mark OS as unsupported by adding UnsupportedReason.OS_VERSION
coresys.resolution.unsupported.append(UnsupportedReason.OS_VERSION)
# Attempt to fetch data should fail due to OS_SUPPORTED condition
with pytest.raises(
UpdaterJobError, match="blocked from execution, unsupported OS version"
):
await coresys.updater.fetch_data()
# Verify that versions were not updated
assert coresys.updater.version_supervisor == initial_supervisor_version
assert coresys.updater.version_homeassistant == initial_homeassistant_version
assert coresys.updater.version_hassos == initial_hassos_version