mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
"""系统后台更新状态机测试。"""
|
|
|
|
import json
|
|
import threading
|
|
import zipfile
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
from app.adapters.system import update as update_module
|
|
|
|
|
|
def _manager(monkeypatch, tmp_path: Path):
|
|
monkeypatch.setattr(
|
|
update_module,
|
|
"get_runtime_setting",
|
|
lambda key: tmp_path if key == "TEMP_PATH" else None,
|
|
)
|
|
manager = object.__new__(update_module.SystemUpdateManager)
|
|
manager._lock = threading.RLock()
|
|
manager._download_active = False
|
|
return manager
|
|
|
|
|
|
def _response(payload, status_code=200):
|
|
return SimpleNamespace(status_code=status_code, json=lambda: payload)
|
|
|
|
|
|
def test_check_exposes_new_stable_release(monkeypatch, tmp_path):
|
|
manager = _manager(monkeypatch, tmp_path)
|
|
releases = [
|
|
{"tag_name": "v3.2.0-beta", "prerelease": True, "draft": False},
|
|
{
|
|
"tag_name": "v3.1.0",
|
|
"name": "MoviePilot v3.1.0",
|
|
"body": "changes",
|
|
"published_at": "2026-08-24T00:00:00Z",
|
|
"prerelease": False,
|
|
"draft": False,
|
|
},
|
|
]
|
|
monkeypatch.setattr(manager, "_request", lambda: SimpleNamespace(get_res=lambda _url: _response(releases)))
|
|
monkeypatch.setattr(update_module, "get_app_version", lambda: "v3.0.0")
|
|
|
|
status = manager.check()
|
|
|
|
assert status.state == "available"
|
|
assert status.version == "v3.1.0"
|
|
assert status.release_notes == "changes"
|
|
assert status.can_update is True
|
|
|
|
|
|
def test_scheduled_check_failure_stays_silent(monkeypatch, tmp_path):
|
|
manager = _manager(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(
|
|
manager,
|
|
"_request",
|
|
lambda: SimpleNamespace(get_res=lambda _url: _response({}, status_code=503)),
|
|
)
|
|
|
|
status = manager.check()
|
|
|
|
assert status.state == "idle"
|
|
assert status.error
|
|
|
|
|
|
def test_interrupted_download_becomes_retryable_failure(monkeypatch, tmp_path):
|
|
manager = _manager(monkeypatch, tmp_path)
|
|
manager._write_state(state="downloading", version="v3.1.0")
|
|
|
|
status = manager.get_status()
|
|
|
|
assert status.state == "failed"
|
|
assert status.can_update is True
|
|
assert "中断" in status.error
|
|
|
|
|
|
def test_download_prepares_matching_backend_and_frontend_archives(monkeypatch, tmp_path):
|
|
manager = _manager(monkeypatch, tmp_path)
|
|
backend_fixture = tmp_path / "source-backend.zip"
|
|
frontend_fixture = tmp_path / "source-frontend.zip"
|
|
with zipfile.ZipFile(backend_fixture, "w") as archive:
|
|
archive.writestr(
|
|
"MoviePilot-v3.1.0/version.py",
|
|
"APP_VERSION = 'v3.1.0'\nFRONTEND_VERSION = 'v3.1.0'\n",
|
|
)
|
|
archive.writestr("MoviePilot-v3.1.0/pyproject.toml", "[project]\n")
|
|
archive.writestr("MoviePilot-v3.1.0/uv.lock", "version = 1\n")
|
|
with zipfile.ZipFile(frontend_fixture, "w") as archive:
|
|
archive.writestr("dist/index.html", "ok")
|
|
archive.writestr("dist/version.txt", "v3.1.0\n")
|
|
|
|
fixtures = iter((backend_fixture, frontend_fixture))
|
|
|
|
def download(_url, destination, downloaded_before, _total_hint):
|
|
source = next(fixtures)
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_bytes(source.read_bytes())
|
|
size = destination.stat().st_size
|
|
return downloaded_before + size, size
|
|
|
|
monkeypatch.setattr(manager, "_download_file", download)
|
|
monkeypatch.setattr(update_module, "is_docker", lambda: True)
|
|
monkeypatch.setattr(
|
|
manager,
|
|
"_fetch_frontend_release",
|
|
lambda _version: {
|
|
"assets": [
|
|
{
|
|
"name": "dist.zip",
|
|
"size": frontend_fixture.stat().st_size,
|
|
"browser_download_url": "https://example.invalid/dist.zip",
|
|
"digest": f"sha256:{manager._sha256(frontend_fixture)}",
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
manager._write_state(state="downloading", version="v3.1.0")
|
|
manager._download_update("v3.1.0")
|
|
|
|
status = manager.get_status()
|
|
prepared = json.loads((manager._root / "prepared.json").read_text(encoding="utf-8"))
|
|
assert status.state == "ready"
|
|
assert status.progress == 100
|
|
assert status.frontend_version == "v3.1.0"
|
|
assert prepared["backend_sha256"] == manager._sha256(manager._backend_archive)
|
|
assert prepared["frontend_sha256"] == manager._sha256(manager._frontend_archive)
|
|
|
|
|
|
def test_request_install_rejects_modified_prepared_package(monkeypatch, tmp_path):
|
|
manager = _manager(monkeypatch, tmp_path)
|
|
manager._root.mkdir(parents=True)
|
|
manager._backend_archive.write_bytes(b"backend")
|
|
manager._frontend_archive.write_bytes(b"frontend")
|
|
(manager._root / "prepared.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"version": "v3.1.0",
|
|
"frontend_version": "v3.1.0",
|
|
"backend_archive": str(manager._backend_archive),
|
|
"frontend_archive": str(manager._frontend_archive),
|
|
"backend_sha256": "invalid",
|
|
"frontend_sha256": manager._sha256(manager._frontend_archive),
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
manager._write_state(state="ready", version="v3.1.0", can_install=True)
|
|
|
|
success, message = manager.request_install()
|
|
|
|
assert success is False
|
|
assert "后端更新包校验失败" in message
|
|
assert not manager._install_file.exists()
|
|
assert manager.get_status().state == "failed"
|
|
|
|
|
|
def test_cancel_install_returns_prepared_update_to_ready(monkeypatch, tmp_path):
|
|
manager = _manager(monkeypatch, tmp_path)
|
|
manager._root.mkdir(parents=True)
|
|
manager._install_file.write_text("{}", encoding="utf-8")
|
|
manager._write_state(state="installing", version="v3.1.0")
|
|
|
|
manager.cancel_install("restart failed")
|
|
|
|
status = manager.get_status()
|
|
assert status.state == "ready"
|
|
assert status.can_install is True
|
|
assert status.error == "restart failed"
|
|
assert not manager._install_file.exists()
|