131 lines
5.3 KiB
Python
131 lines
5.3 KiB
Python
"""Tests for services.installer — no GUI/network (download is mocked)."""
|
|||
|
|
import hashlib
|
||
|
|
import shutil
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
import zipfile
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||
|
|
|
||
|
|
from services import installer
|
||
|
|
from services.update_service import UpdateInfo
|
||
|
|
|
||
|
|
|
||
|
|
def _make_release_zip(dst_zip, version, exe_body, wrap=False):
|
||
|
|
tmp = Path(tempfile.mkdtemp())
|
||
|
|
try:
|
||
|
|
base = tmp / ("CMBot-" + version) if wrap else tmp
|
||
|
|
base.mkdir(parents=True, exist_ok=True)
|
||
|
|
(base / "CMBot.exe").write_text(exe_body, encoding="ascii")
|
||
|
|
(base / "version.txt").write_text(version, encoding="ascii")
|
||
|
|
with zipfile.ZipFile(str(dst_zip), "w", zipfile.ZIP_DEFLATED) as z:
|
||
|
|
for p in base.rglob("*"):
|
||
|
|
z.write(str(p), str(p.relative_to(tmp)))
|
||
|
|
finally:
|
||
|
|
shutil.rmtree(str(tmp), ignore_errors=True)
|
||
|
|
return hashlib.sha256(dst_zip.read_bytes()).hexdigest()
|
||
|
|
|
||
|
|
|
||
|
|
class _Base(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.tmp = Path(tempfile.mkdtemp())
|
||
|
|
self.root = self.tmp / "install"
|
||
|
|
self.app = self.root / "app"
|
||
|
|
self.app.mkdir(parents=True)
|
||
|
|
(self.app / "CMBot.exe").write_text("OLD-1.0.0", encoding="ascii")
|
||
|
|
(self.app / "version.txt").write_text("1.0.0", encoding="ascii")
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
shutil.rmtree(str(self.tmp), ignore_errors=True)
|
||
|
|
|
||
|
|
def _info(self, version, zip_path):
|
||
|
|
return UpdateInfo(version=version, source="http://x/{}.zip".format(version),
|
||
|
|
sha256=hashlib.sha256(zip_path.read_bytes()).hexdigest(),
|
||
|
|
size=zip_path.stat().st_size)
|
||
|
|
|
||
|
|
|
||
|
|
class TestDownloadAndStage(_Base):
|
||
|
|
def _copy_download(self, src_zip):
|
||
|
|
def fake(url, dest, user="", password="", timeout=120):
|
||
|
|
shutil.copy(str(src_zip), str(dest))
|
||
|
|
return fake
|
||
|
|
|
||
|
|
def test_stage_flat_zip(self):
|
||
|
|
z = self.tmp / "n.zip"
|
||
|
|
_make_release_zip(z, "1.1.0", "NEW")
|
||
|
|
with patch("services.installer.download", side_effect=self._copy_download(z)):
|
||
|
|
ver = installer.download_and_stage(self.root, self._info("1.1.0", z))
|
||
|
|
self.assertEqual(ver, "1.1.0")
|
||
|
|
self.assertEqual(installer.staged_version(self.root), "1.1.0")
|
||
|
|
self.assertTrue((installer.staged_app(self.root) / "CMBot.exe").exists())
|
||
|
|
# app untouched until apply
|
||
|
|
self.assertEqual((self.app / "version.txt").read_text(encoding="ascii"), "1.0.0")
|
||
|
|
|
||
|
|
def test_stage_wrapped_zip(self):
|
||
|
|
z = self.tmp / "n.zip"
|
||
|
|
_make_release_zip(z, "1.1.0", "NEW", wrap=True)
|
||
|
|
with patch("services.installer.download", side_effect=self._copy_download(z)):
|
||
|
|
ver = installer.download_and_stage(self.root, self._info("1.1.0", z))
|
||
|
|
self.assertEqual(ver, "1.1.0")
|
||
|
|
self.assertEqual(installer.staged_version(self.root), "1.1.0")
|
||
|
|
|
||
|
|
def test_sha_mismatch_raises(self):
|
||
|
|
z = self.tmp / "n.zip"
|
||
|
|
_make_release_zip(z, "1.1.0", "NEW")
|
||
|
|
info = self._info("1.1.0", z)
|
||
|
|
info.sha256 = "deadbeef"
|
||
|
|
with patch("services.installer.download", side_effect=self._copy_download(z)):
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
installer.download_and_stage(self.root, info)
|
||
|
|
self.assertEqual(installer.staged_version(self.root), "")
|
||
|
|
|
||
|
|
def test_version_mismatch_raises(self):
|
||
|
|
z = self.tmp / "n.zip"
|
||
|
|
_make_release_zip(z, "1.1.0", "NEW")
|
||
|
|
info = self._info("9.9.9", z) # manifest claims a different version
|
||
|
|
with patch("services.installer.download", side_effect=self._copy_download(z)):
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
installer.download_and_stage(self.root, info)
|
||
|
|
|
||
|
|
|
||
|
|
class TestApplyStaged(_Base):
|
||
|
|
def _stage(self, version, body):
|
||
|
|
s = installer.staged_app(self.root)
|
||
|
|
s.mkdir(parents=True, exist_ok=True)
|
||
|
|
(s / "CMBot.exe").write_text(body, encoding="ascii")
|
||
|
|
(s / "version.txt").write_text(version, encoding="ascii")
|
||
|
|
|
||
|
|
def test_apply_swaps(self):
|
||
|
|
self._stage("1.1.0", "NEW-1.1.0")
|
||
|
|
applied = installer.apply_staged(self.root)
|
||
|
|
self.assertEqual(applied, "1.1.0")
|
||
|
|
self.assertEqual((self.app / "version.txt").read_text(encoding="ascii"), "1.1.0")
|
||
|
|
self.assertEqual((self.app / "CMBot.exe").read_text(encoding="ascii"), "NEW-1.1.0")
|
||
|
|
self.assertEqual((installer.old_dir(self.root) / "version.txt").read_text(encoding="ascii"), "1.0.0")
|
||
|
|
self.assertEqual(installer.staged_version(self.root), "")
|
||
|
|
|
||
|
|
def test_nothing_staged_returns_empty(self):
|
||
|
|
self.assertEqual(installer.apply_staged(self.root), "")
|
||
|
|
self.assertEqual((self.app / "version.txt").read_text(encoding="ascii"), "1.0.0")
|
||
|
|
|
||
|
|
def test_not_newer_staged_is_discarded(self):
|
||
|
|
self._stage("0.9.0", "OLDER")
|
||
|
|
self.assertEqual(installer.apply_staged(self.root), "")
|
||
|
|
self.assertEqual((self.app / "version.txt").read_text(encoding="ascii"), "1.0.0")
|
||
|
|
self.assertFalse(installer.staged_app(self.root).exists())
|
||
|
|
|
||
|
|
|
||
|
|
class TestHelpers(_Base):
|
||
|
|
def test_is_writable(self):
|
||
|
|
self.assertTrue(installer.is_writable(self.root))
|
||
|
|
|
||
|
|
def test_install_root_for(self):
|
||
|
|
self.assertEqual(installer.install_root_for(self.app), self.root.resolve())
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|