Files
site/WESP_REL/tests/test_update_flow.py
T
2026-07-17 12:57:18 +03:00

110 lines
4.1 KiB
Python

"""Integration tests for OTA update flow (mocked network)."""
from __future__ import annotations
import json
import os
import shutil
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest.mock import MagicMock, patch
from update import AutoUpdater
class UpdateFlowTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.mkdtemp(prefix="wesp-flow-")
self.root = Path(self._tmp)
data = self.root / "data"
data.mkdir(parents=True)
(data / "config.json").write_text(json.dumps({"version": "1.0.0"}), encoding="utf-8")
(self.root / "requirements.txt").write_text("Flask==2.3.3\n", encoding="utf-8")
wheels = self.root / "vendor" / "wheels"
wheels.mkdir(parents=True)
(wheels / "flask-2.3.3-py3-none-any.whl").write_bytes(b"x")
(self.root / "wsgi.py").write_text("app = object()\n", encoding="utf-8")
def tearDown(self) -> None:
shutil.rmtree(self._tmp, ignore_errors=True)
def _minimal_updater(self) -> AutoUpdater:
with patch.object(AutoUpdater, "__init__", lambda self: None):
u = AutoUpdater()
u.base_dir = str(self.root)
u.config_file = str(self.root / "data" / "config.json")
u.enabled = True
u.current_version = "1.0.0"
u.is_updating = False
u.pending_release = None
u.update_progress = None
u._state_lock = __import__("threading").Lock()
u.restart_cmd = "true"
u.protected_files = [
"data/recipes.db",
"data/update_state.json",
"data/.pending_restart.json",
]
u.protected_folders = ["backups", "temp_updates", ".secret"]
return u
def _make_zip(self, path: Path) -> None:
staging = self.root / "zip_staging" / "wesp"
staging.mkdir(parents=True)
shutil.copy(self.root / "requirements.txt", staging / "requirements.txt")
shutil.copytree(self.root / "vendor", staging / "vendor")
(staging / "run.py").write_text("# stub\n", encoding="utf-8")
with zipfile.ZipFile(path, "w") as zf:
for f in staging.rglob("*"):
if f.is_file():
zf.write(f, f.relative_to(staging.parent))
@patch("update.AutoUpdater._run_restart_cmd")
@patch("app.services.update_verify.run_smoke_test", return_value=(True, "OK"))
@patch("app.services.update_verify.install_requirements_from_package")
@patch("update.AutoUpdater.download_update")
@patch("update.AutoUpdater.create_backup")
def test_full_success(
self,
mock_backup,
mock_download,
mock_install,
mock_smoke,
mock_restart,
) -> None:
backup_dir = self.root / "backups" / "b1"
backup_dir.mkdir(parents=True)
mock_backup.return_value = str(backup_dir)
zip_path = self.root / "temp_updates" / "u.zip"
zip_path.parent.mkdir(parents=True)
self._make_zip(zip_path)
mock_download.return_value = str(zip_path)
mock_smoke.return_value = (True, "OK")
u = self._minimal_updater()
release = {"version": "2.0.0", "assets": []}
self.assertTrue(u.update(release))
mock_install.assert_called_once()
mock_restart.assert_called_once()
@patch("update.AutoUpdater.create_backup")
@patch("update.AutoUpdater.download_update")
@patch("update.AutoUpdater.apply_update", return_value=True)
@patch("app.services.update_verify.wheelhouse_covers_requirements", return_value=(False, ["flask"]))
def test_deps_missing_rollback(self, _wh, _apply, mock_dl, mock_bk) -> None:
bk = self.root / "backups" / "b2"
bk.mkdir(parents=True)
(bk / "run.py").write_text("old\n", encoding="utf-8")
mock_bk.return_value = str(bk)
mock_dl.return_value = str(self.root / "z.zip")
u = self._minimal_updater()
with patch.object(u, "_restore_from_backup") as mock_restore:
self.assertFalse(u.update({"version": "2.0.0"}))
mock_restore.assert_called()
if __name__ == "__main__":
unittest.main()