139 lines
4.8 KiB
Python
139 lines
4.8 KiB
Python
"""自动升级清单、下载与校验测试。"""
|
|
|
|
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()
|