BUH_bot/tests/release_updater_test.py
Игорь Надеин 5155e455d2
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Add Gitea release updates and local Windows release pipeline
2026-09-16 15:03:17 +03:00

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()