Initial SyncVito release with installer updater

This commit is contained in:
Davito committed 2026-09-23 19:40:36 +02:00
commit b9e4c09f49
41 files changed
+4331

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
import base64
import hashlib
import os
from pathlib import Path
import pytest
import syncvito.updater as updater
from syncvito.updater import GiteaUpdateService, UpdateFormatError, UpdateInfo, parse_version
class FakeResponse:
def __init__(self, payload: bytes, url: str = "https://gitea.example/asset") -> None:
self.payload = payload
self.offset = 0
self.headers = {"Content-Length": str(len(payload))}
self.url = url
def __enter__(self) -> "FakeResponse":
return self
def __exit__(self, *_args: object) -> bool:
return False
def read(self, size: int = -1) -> bytes:
if size < 0:
size = len(self.payload) - self.offset
chunk = self.payload[self.offset : self.offset + size]
self.offset += len(chunk)
return chunk
def geturl(self) -> str:
return self.url
def _release(version: str, *, prerelease: bool = False) -> dict[str, object]:
installer = f"SyncVitoSetup-{version}.exe"
return {
"tag_name": f"v{version}",
"name": f"SyncVito {version}",
"body": "Änderungen",
"html_url": f"https://gitea.example/releases/v{version}",
"draft": False,
"prerelease": prerelease,
"assets": [
{
"name": installer,
"browser_download_url": f"https://gitea.example/download/{installer}",
"size": 1234,
},
{
"name": f"{installer}.sha256",
"browser_download_url": f"https://gitea.example/download/{installer}.sha256",
"size": 96,
},
],
}
def _info(version: str = "1.1.0") -> UpdateInfo:
installer = f"SyncVitoSetup-{version}.exe"
return UpdateInfo(
version=version,
title=f"SyncVito {version}",
notes="Änderungen",
release_url=f"https://gitea.example/releases/v{version}",
installer_name=installer,
download_url=f"https://gitea.example/download/{installer}",
checksum_url=f"https://gitea.example/download/{installer}.sha256",
size=0,
)
def test_parse_version_accepts_stable_tags() -> None:
assert parse_version("v1.2.3") == (1, 2, 3)
assert parse_version("10.20.30") == (10, 20, 30)
def test_parse_version_rejects_prerelease() -> None:
with pytest.raises(ValueError):
parse_version("1.2.3-beta")
def test_check_reads_latest_installer_release(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
service = GiteaUpdateService("1.0.0", base_url="https://gitea.example", update_directory=tmp_path)
monkeypatch.setattr(service, "_read_json", lambda _url: _release("1.3.0"))
update = service.check()
assert update is not None
assert update.version == "1.3.0"
assert update.installer_name == "SyncVitoSetup-1.3.0.exe"
def test_check_returns_none_for_current_version(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
service = GiteaUpdateService("1.3.0", base_url="https://gitea.example", update_directory=tmp_path)
monkeypatch.setattr(service, "_read_json", lambda _url: _release("1.3.0"))
assert service.check() is None
def test_release_requires_matching_installer(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
service = GiteaUpdateService("1.0.0", base_url="https://gitea.example", update_directory=tmp_path)
release = _release("1.1.0")
release["assets"] = []
monkeypatch.setattr(service, "_read_json", lambda _url: release)
with pytest.raises(UpdateFormatError, match="SyncVitoSetup-1.1.0.exe"):
service.check()
def test_download_verifies_checksum(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
service = GiteaUpdateService("1.0.0", base_url="https://gitea.example", update_directory=tmp_path)
payload = b"MZ-syncvito-installer"
digest = hashlib.sha256(payload).hexdigest()
responses = iter(
[
FakeResponse(f"{digest} *SyncVitoSetup-1.1.0.exe\n".encode()),
FakeResponse(payload),
]
)
monkeypatch.setattr(service, "_request", lambda *_args, **_kwargs: next(responses))
progress: list[tuple[int, str]] = []
result = service.download(_info(), lambda value, text: progress.append((value, text)))
assert result.read_bytes() == payload
assert progress[-1][0] == 100
def test_cleanup_removes_only_managed_installers(tmp_path: Path) -> None:
service = GiteaUpdateService("1.0.0", base_url="https://gitea.example", update_directory=tmp_path)
managed = [tmp_path / "SyncVitoSetup-1.0.1.exe", tmp_path / "SyncVitoSetup-1.0.2.exe.part"]
preserved = [tmp_path / "anderes-setup.exe", tmp_path / "SyncVitoSetup-neu.exe"]
for path in managed + preserved:
path.write_bytes(b"test")
assert service.cleanup_installers() == (2, 0)
assert all(not path.exists() for path in managed)
assert all(path.exists() for path in preserved)
def test_update_watcher_waits_before_installer(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
installer = tmp_path / "SyncVitoSetup-1.1.0.exe"
bootstrap_log = tmp_path / "bootstrap.log"
captured: dict[str, object] = {}
def capture(executable: str, arguments: list[str]) -> None:
captured["executable"] = executable
captured["arguments"] = arguments
monkeypatch.setattr(updater, "_start_detached", capture)
monkeypatch.setattr(updater.os, "name", "nt")
powershell = Path(os.environ.get("SYSTEMROOT", r"C:\Windows")) / "System32/WindowsPowerShell/v1.0/powershell.exe"
monkeypatch.setattr(Path, "is_file", lambda self: self == powershell)
updater._start_installer_after_exit(installer, ["/VERYSILENT"], [222, 111], bootstrap_log)
arguments = captured["arguments"]
assert isinstance(arguments, list)
encoded = arguments[arguments.index("-EncodedCommand") + 1]
script = base64.b64decode(encoded).decode("utf-16-le")
assert "foreach ($processId in @(111, 222))" in script
assert script.index("while (Get-Process") < script.index(str(installer))
assert "'/VERYSILENT'" in script