Adds the PyQt6 Windows client (Homepage, Scan & results, About & updates, Settings), ClamAV scanning and quarantine, browser-download watch with an exclusive scan gate, Bolt loopback/PKCE sign-in with DPAPI key storage, Gitea release feed with checksum verification, opt-in diagnostics, and Windows/portable build tooling. Co-Authored-By: Claude Code <noreply@anthropic.com>
122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from boltclam.models import UpdateAsset, UpdateFeed, UpdateRelease
|
|
from boltclam.updates import (
|
|
UpdateError,
|
|
UpdateService,
|
|
choose_asset,
|
|
is_newer,
|
|
parse_checksums,
|
|
parse_version,
|
|
)
|
|
|
|
|
|
def test_version_parsing_and_comparison():
|
|
assert parse_version("v1.2.3") == ((1, 2, 3), ())
|
|
assert parse_version("1.2.3-beta.1")
|
|
assert is_newer("v1.2.0", "1.1.9") is True
|
|
assert is_newer("v1.0.0", "1.0.0") is False
|
|
assert is_newer("v1.0.0", "1.0.0-beta.2") is True
|
|
assert is_newer("v1.0.0-beta.2", "1.0.0") is False
|
|
assert is_newer("v1.0.0-beta.2", "1.0.0-beta.1") is True
|
|
assert is_newer("not-a-version", "1.0.0") is False
|
|
|
|
|
|
def _release(tag="v1.2.0", prerelease=False, assets=None) -> UpdateRelease:
|
|
return UpdateRelease(id="2", tag=tag, name="Test", notes="notes",
|
|
prerelease=prerelease, published_at="2026-10-20T00:00:00Z",
|
|
html_url="https://git.example", assets=assets or [])
|
|
|
|
|
|
def test_choose_asset_prefers_a_setup_executable():
|
|
release = _release(assets=[
|
|
UpdateAsset("1", "BoltClam-1.2.0.zip", 5, "application/zip", "u1"),
|
|
UpdateAsset("2", "BoltClam-1.2.0-Setup.exe", 5, "application/octet-stream", "u2"),
|
|
])
|
|
assert choose_asset(release).name == "BoltClam-1.2.0-Setup.exe"
|
|
|
|
|
|
def test_choose_asset_falls_back_to_zip():
|
|
release = _release(assets=[
|
|
UpdateAsset("1", "notes.txt", 5, "text/plain", "u1"),
|
|
UpdateAsset("2", "BoltClam-windows.zip", 5, "application/zip", "u2"),
|
|
])
|
|
assert choose_asset(release).name == "BoltClam-windows.zip"
|
|
assert choose_asset(_release(assets=[])) is None
|
|
|
|
|
|
def test_feed_from_api_preserves_release_fields():
|
|
feed = UpdateFeed.from_api({
|
|
"channel": "beta",
|
|
"latest": {"id": "3", "tag": "v2.0.0-beta", "name": "Beta",
|
|
"notes": "notes", "prerelease": True,
|
|
"published_at": "2026-10-22T00:00:00Z",
|
|
"assets": [{"id": "9", "name": "a.exe", "size": 12,
|
|
"content_type": "x", "download_url": "u"}]},
|
|
"releases": [],
|
|
"checked_at": "2026-10-25T00:00:00Z",
|
|
})
|
|
assert feed.channel == "beta"
|
|
assert feed.latest.tag == "v2.0.0-beta"
|
|
assert feed.latest.assets[0].name == "a.exe"
|
|
|
|
|
|
def test_parse_checksums():
|
|
digest = "a" * 64
|
|
assert parse_checksums(f"{digest} BoltClam-Setup.exe\n") == {
|
|
"BoltClam-Setup.exe": digest}
|
|
|
|
|
|
class FakeApi:
|
|
def __init__(self, checksum_text: str | None = None, payload: bytes = b"payload"):
|
|
self.checksum_text = checksum_text
|
|
self.payload = payload
|
|
|
|
def download_asset(self, url, destination, progress=None):
|
|
destination = Path(destination)
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
data = self.checksum_text.encode() if self.checksum_text and "SUM" in url else self.payload
|
|
destination.write_bytes(data)
|
|
return destination, hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def _download_release(checksum: str) -> UpdateRelease:
|
|
asset = UpdateAsset("1", "BoltClam-Setup.exe", 7, "application/octet-stream",
|
|
"https://api.example/asset")
|
|
checksum_asset = UpdateAsset("2", "SHA256SUMS.txt", 100, "text/plain",
|
|
"https://api.example/SUM")
|
|
return _release(assets=[asset, checksum_asset])
|
|
|
|
|
|
def test_download_verifies_a_matching_checksum(isolated_env):
|
|
payload = b"payload"
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
release = _download_release(digest)
|
|
service = UpdateService(FakeApi(f"{digest} BoltClam-Setup.exe\n", payload))
|
|
result = service.download(release, release.assets[0])
|
|
assert result.path.read_bytes() == payload
|
|
assert result.verified is True
|
|
assert result.expected_sha256 == digest
|
|
|
|
|
|
def test_download_rejects_a_bad_checksum(isolated_env):
|
|
release = _download_release("b" * 64)
|
|
service = UpdateService(FakeApi(f"{'b' * 64} BoltClam-Setup.exe\n", b"payload"))
|
|
with pytest.raises(UpdateError):
|
|
service.download(release, release.assets[0])
|
|
|
|
|
|
def test_check_returns_the_feed(isolated_env):
|
|
class Api:
|
|
def get_updates(self, channel, force=False):
|
|
return {"channel": channel, "latest": None, "releases": []}
|
|
|
|
feed = UpdateService(Api(), "beta").check()
|
|
assert feed.channel == "beta"
|
|
assert feed.latest is None
|