Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""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()
|