"""Pending updates on OpenWrt: never "nothing pending" when the index is gone. OpenWrt keeps its package index in RAM. After a reboot ``apk version`` only warns that it cannot open the cached index and exits 0, and ``opkg list-upgradable`` prints nothing: both looked like "no updates". netOrk keeps "pending since" per package, so a reader that cannot read raises instead, and ``refresh_available_updates`` loads the index (netOrk MVP 5). The apk warning is the real output of an OpenWrt 25.12.5 access point after a reboot. """ from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from napalm_openwrt.openwrt import OpenWrtDriver APK_INDEX_MISSING = ( "WARNING: opening from cache https://downloads.openwrt.org/releases/25.12.5/targets/" "ath79/generic/packages/packages.adb: No such file or directory\n" "WARNING: opening from cache https://downloads.openwrt.org/releases/25.12.5/packages/" "mips_24kc/base/packages.adb: No such file or directory\n" "__OWRT_RC=0\n" ) APK_PENDING = ( "Installed: Available:\n" "dnsmasq-2.90-r4 < 2.90-r5\n" "hostapd-common-2024.09.15-r2 < 2024.09.15-r3\n" "__OWRT_RC=0\n" ) OPKG_PENDING = "dnsmasq - 2.90-4 - 2.90-5\n" @pytest.fixture def driver(): with patch("napalm_openwrt.openwrt.ConnectHandler"): drv = OpenWrtDriver(hostname="192.168.1.1", username="root", password="") drv.device = MagicMock() yield drv def _answer(driver, pm: str, reply: str): """Answer the package-manager probe with *pm* and everything else with *reply*.""" sent: list = [] def send(cmd, **kwargs): sent.append(cmd) if cmd.startswith("command -v apk"): return "/usr/bin/apk" if pm == "apk" else "__no_apk__" return reply driver._send_command = send return sent class TestAvailableUpdates: def test_apk_without_its_index_raises_instead_of_reporting_nothing(self, driver): _answer(driver, "apk", APK_INDEX_MISSING) with pytest.raises(RuntimeError, match="index"): driver.get_available_updates() def test_apk_with_its_index_lists_what_is_pending(self, driver): _answer(driver, "apk", APK_PENDING) updates = driver.get_available_updates() assert [u["name"] for u in updates] == ["dnsmasq", "hostapd-common"] assert updates[0]["current_version"] == "2.90-r4" assert updates[0]["new_version"] == "2.90-r5" def test_apk_that_failed_raises(self, driver): _answer(driver, "apk", "ERROR: unable to lock database\n__OWRT_RC=99\n") with pytest.raises(RuntimeError): driver.get_available_updates() def test_opkg_without_its_lists_raises(self, driver): _answer(driver, "opkg", "__NO_INDEX__\n") with pytest.raises(RuntimeError, match="index"): driver.get_available_updates() def test_opkg_with_its_lists(self, driver): _answer(driver, "opkg", OPKG_PENDING) assert driver.get_available_updates() == [ {"name": "dnsmasq", "current_version": "2.90-4", "new_version": "2.90-5"} ] class TestRefresh: def test_apk_loads_its_index(self, driver): sent = _answer(driver, "apk", "OK: 8912 distinct packages available\n__OWRT_RC=0\n") assert driver.refresh_available_updates()["success"] is True assert sent[-1].startswith("apk update") def test_opkg_loads_its_lists(self, driver): sent = _answer(driver, "opkg", "Updated list of available packages\n__OWRT_RC=0\n") assert driver.refresh_available_updates()["success"] is True assert sent[-1].startswith("opkg update") def test_a_failed_refresh_says_why(self, driver): _answer(driver, "apk", "ERROR: network unreachable\n__OWRT_RC=1\n") result = driver.refresh_available_updates() assert result == {"success": False, "output": "ERROR: network unreachable"}