"""自动升级清单、下载与校验测试。""" from __future__ import annotations import hashlib import io import tempfile import unittest from pathlib import Path from unittest import mock import backend_client import desktop_updater class _DownloadResponse: def __init__(self, content: bytes, *, status: int = 200): self._stream = io.BytesIO(content) self.status = status self.headers = {"Content-Length": str(len(content))} def read(self, size: int = -1) -> bytes: return self._stream.read(size) def __enter__(self): return self def __exit__(self, *_args): return False class DesktopUpdaterTest(unittest.TestCase): def setUp(self) -> None: self.temporary = tempfile.TemporaryDirectory() self.root = Path(self.temporary.name) self.patches = [ mock.patch.object(desktop_updater, "UPDATE_ROOT", self.root), mock.patch.object( desktop_updater, "UPDATE_STATE_FILE", self.root / "state.json" ), mock.patch.object( desktop_updater, "UPDATE_RESULT_FILE", self.root / "result.json" ), ] for patcher in self.patches: patcher.start() def tearDown(self) -> None: for patcher in reversed(self.patches): patcher.stop() self.temporary.cleanup() def test_manifest_respects_server_eligibility_and_semantic_versions(self) -> None: response = { "latest_version": "2.0.0", "download_url": "https://dl.example.com/update.exe", "package_sha256": "a" * 64, "package_size": 123, "auto_install": True, "eligible": False, "update_available": False, } with mock.patch.object( backend_client, "_request", return_value=(200, response) ) as request: manifest = desktop_updater.fetch_manifest( "https://admin.example.com", "device-1", current_version="1.1.2" ) self.assertFalse(manifest["eligible"]) self.assertFalse(manifest["update_available"]) self.assertFalse(manifest["auto_install"]) self.assertIn("current_version=1.1.2", request.call_args.args[2]) def test_download_is_kept_only_after_sha256_verification(self) -> None: content = b"MZ" + b"verified-installer" * 100 progress = [] manifest = { "latest_version": "2.0.0", "download_url": "http://127.0.0.1:8766/update.exe", "package_sha256": hashlib.sha256(content).hexdigest(), "package_size": len(content), "signature_required": False, } with ( mock.patch.object( desktop_updater.urllib.request, "urlopen", return_value=_DownloadResponse(content), ), mock.patch.object( desktop_updater, "authenticode_status", return_value="NotSigned" ), ): installer = desktop_updater.download_update( manifest, lambda downloaded, total: progress.append((downloaded, total)) ) self.assertEqual(installer.read_bytes(), content) self.assertTrue(progress) self.assertTrue((self.root / "state.json").is_file()) def test_bad_hash_is_deleted_and_never_becomes_an_installer(self) -> None: content = b"MZ-corrupted" manifest = { "latest_version": "2.0.0", "download_url": "http://localhost/update.exe", "package_sha256": "0" * 64, "package_size": len(content), } with mock.patch.object( desktop_updater.urllib.request, "urlopen", return_value=_DownloadResponse(content), ): with self.assertRaises(desktop_updater.UpdateError): desktop_updater.download_update(manifest) version_dir = self.root / "2.0.0" self.assertFalse((version_dir / "ZhenAI-Update-v2.0.0.exe").exists()) self.assertFalse((version_dir / "ZhenAI-Update-v2.0.0.exe.part").exists()) def test_plain_http_is_rejected_for_remote_servers(self) -> None: with self.assertRaises(desktop_updater.UpdateError): desktop_updater._validated_download_url( "http://downloads.example.com/update.exe" ) def test_source_checkout_never_overwrites_an_installation(self) -> None: installer = self.root / "update.exe" installer.write_bytes(b"MZ") with mock.patch.object(desktop_updater.sys, "frozen", False, create=True): with self.assertRaises(desktop_updater.UpdateError): desktop_updater.launch_installer(installer, "2.0.0") if __name__ == "__main__": unittest.main()