"""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