import unittest from unittest import mock from PySide6.QtWidgets import QApplication, QDialog import startup_update as update class StartupUpdateTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) def setUp(self): self.dialog = QDialog() self.dialog._busy = mock.Mock() self.dialog._set_status = mock.Mock() self.check = update.LoginUpdateCheck(self.dialog) self.resume = mock.Mock() self.check.resume = self.resume self.check.phase = "checking" for name, value in (("load_settings", {"server_url": "https://backend.example"}), ("cached_release_status", {})): patcher = mock.patch.object(update.backend_client, name, return_value=value) patcher.start() self.addCleanup(patcher.stop) patcher = mock.patch.object(update.backend_client, "save_settings") patcher.start() self.addCleanup(patcher.stop) self.addCleanup(self.dialog.close) def release(self, **values): return {"latest_version": "99.0.0", "update_available": True, "eligible": True, "ready_for_auto_update": True, **values} def test_no_update_continues_without_prompt(self): with mock.patch.object(self.check, "_offer") as offer: self.check._checked({"release": {"update_available": False}}) self.resume.assert_called_once() offer.assert_not_called() def test_ineligible_release_does_not_prompt(self): with mock.patch.object(self.check, "_offer") as offer: self.check._checked({"release": self.release(eligible=False)}) self.resume.assert_called_once() offer.assert_not_called() def test_timeout_continues_once_and_ignores_late_results(self): self.check._timeout() self.check._checked({"release": self.release(force_upgrade=True)}) self.resume.assert_called_once() def test_network_failure_continues_login(self): self.check._checked({"error": "offline"}) self.resume.assert_called_once() def test_known_forced_upgrade_is_preserved_offline(self): with mock.patch.object(update.backend_client, "cached_release_status", return_value=self.release(force_upgrade=True)), \ mock.patch.object(self.check, "_offer") as offer: self.check._checked({"error": "offline"}) offer.assert_called_once() self.resume.assert_not_called() def test_available_update_prompts_before_login(self): with mock.patch.object(self.check, "_offer") as offer: self.check._checked({"release": self.release()}) offer.assert_called_once() self.resume.assert_not_called() def test_auto_install_policy_starts_downloading(self): with mock.patch.object(self.check, "_download") as download: self.check._checked({"release": self.release(auto_install=True)}) download.assert_called_once() self.resume.assert_not_called() def test_optional_update_can_be_deferred(self): self.check.release = self.release() with mock.patch.object(update, "QMessageBox") as box: box.return_value.clickedButton.return_value = None self.check._offer() self.resume.assert_called_once() def test_forced_update_cannot_be_deferred(self): self.check.release = self.release(force_upgrade=True) with mock.patch.object(update, "QMessageBox") as box: box.return_value.clickedButton.return_value = None self.check._offer() self.assertEqual(self.check.phase, "closed") self.resume.assert_not_called() def test_downloaded_update_launches_helper_and_exits_login(self): self.check.phase = "downloading" self.check.release = self.release() with mock.patch.object(update.desktop_updater, "launch_installer") as launch: self.check._downloaded({"installer": "probe.exe"}) launch.assert_called_once() self.assertEqual(self.check.phase, "closed") self.resume.assert_not_called() def test_optional_download_failure_allows_login(self): self.check.phase = "downloading" self.check.release = self.release() with mock.patch.object(update.QMessageBox, "warning"): self.check._downloaded({"error": "hash mismatch"}) self.resume.assert_called_once() def test_forced_download_failure_does_not_allow_login(self): self.check.phase = "downloading" self.check.release = self.release(force_upgrade=True) with mock.patch.object(update.QMessageBox, "warning"): self.check._downloaded({"error": "hash mismatch"}) self.resume.assert_not_called() self.assertEqual(self.check.phase, "closed") def test_closed_window_ignores_download_completion(self): self.check._closed() with mock.patch.object(update.desktop_updater, "launch_installer") as launch: self.check._downloaded({"installer": "probe.exe"}) launch.assert_not_called() def test_check_starts_without_login_and_uses_public_manifest(self): self.check.phase = "idle" with mock.patch.object(update.threading, "Thread") as thread, \ mock.patch.object(update.backend_client, "device_id", return_value="probe-device"), \ mock.patch.object(update.desktop_updater, "fetch_manifest", return_value={"update_available": False}) as fetch: self.check.start(self.resume) self.resume.assert_not_called() thread.call_args.kwargs["target"]() fetch.assert_called_once_with("https://backend.example", "probe-device", timeout=6.0) self.resume.assert_called_once() if __name__ == "__main__": unittest.main()