110 lines
4.1 KiB
Python
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()
|