Files
TheMajesticMagicianandClaude Code addd00364d BoltClam 1.0.0: ClamAV desktop protection with Bolt sign-in
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>
2026-10-25 16:21:14 -06:00

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