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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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
|
||||
Reference in New Issue
Block a user