"""Release metadata and installer integrity checks.""" import hashlib import tempfile import unittest from pathlib import Path from unittest.mock import patch from packaging.version import Version from app_version import CHECKSUM_ASSET_NAME, INSTALLER_ASSET_NAME from modules.release_updater import ( ReleaseInfo, UpdateError, download_installer, get_latest_release, update_available, ) HOST = "https://git.robotail.pro" class FakeResponse: def __init__(self, *, status=200, url=HOST, data=None, text="", chunks=()): self.status_code = status self.url = url self.data = data self.text = text self.chunks = chunks def raise_for_status(self): if self.status_code >= 400: raise ValueError(f"HTTP {self.status_code}") def json(self): return self.data def iter_content(self, chunk_size): return iter(self.chunks) def __enter__(self): return self def __exit__(self, *_): return False class FakeSession: def __init__(self, responses): self.responses = iter(responses) def get(self, *_args, **_kwargs): return next(self.responses) def release_data(installer_url=HOST + "/download/setup"): return { "tag_name": "v1.2.0", "body": "Исправления и новые возможности", "html_url": HOST + "/Robotail/BUH_bot/releases/tag/v1.2.0", "assets": [ {"name": INSTALLER_ASSET_NAME, "browser_download_url": installer_url, "size": 7}, {"name": CHECKSUM_ASSET_NAME, "browser_download_url": HOST + "/download/sha", "size": 90}, ], } class ReleaseUpdaterTests(unittest.TestCase): def test_no_published_release(self): self.assertIsNone(get_latest_release(FakeSession([FakeResponse(status=404)]))) def test_newer_release_with_assets(self): release = get_latest_release(FakeSession([FakeResponse(data=release_data())])) self.assertEqual(release.version, Version("1.2.0")) self.assertTrue(update_available(release)) def test_rejects_untrusted_installer_host(self): with self.assertRaises(UpdateError): get_latest_release(FakeSession([FakeResponse(data=release_data("https://evil.example/setup"))])) def test_rejects_missing_checksum(self): data = release_data() data["assets"].pop() with self.assertRaises(UpdateError): get_latest_release(FakeSession([FakeResponse(data=data)])) def test_download_and_verify(self): payload = b"installer" digest = hashlib.sha256(payload).hexdigest() release = ReleaseInfo( "v1.2.0", Version("1.2.0"), "", HOST + "/release", HOST + "/download/setup", HOST + "/download/sha", len(payload), ) session = FakeSession([ FakeResponse(url=release.checksum_url, text=f"{digest} {INSTALLER_ASSET_NAME}"), FakeResponse(url=release.installer_url, chunks=[payload[:3], payload[3:]]), ]) with tempfile.TemporaryDirectory() as directory, patch( "modules.release_updater.tempfile.gettempdir", return_value=directory ): target = download_installer(release, session) self.assertEqual(target.read_bytes(), payload) def test_rejects_corrupt_installer(self): payload = b"corrupt" release = ReleaseInfo( "v1.2.0", Version("1.2.0"), "", HOST + "/release", HOST + "/download/setup", HOST + "/download/sha", len(payload), ) session = FakeSession([ FakeResponse(url=release.checksum_url, text="0" * 64), FakeResponse(url=release.installer_url, chunks=[payload]), ]) with tempfile.TemporaryDirectory() as directory, patch( "modules.release_updater.tempfile.gettempdir", return_value=directory ): with self.assertRaises(UpdateError): download_installer(release, session) self.assertFalse((Path(directory) / "MoySkladGenerator" / "v1.2.0" / INSTALLER_ASSET_NAME).exists()) if __name__ == "__main__": unittest.main()