Geführtes Update: "Anwendung ersetzen" im Update-Hinweis
Der Update-Hinweis bietet neben "Update Ordner öffnen" nun den Button "Anwendung ersetzen". Er kopiert das ZIP vom SMB-Laufwerk lokal, prüft es, entpackt es in einen eigenen Ordner neben der aktuellen Installation, übernimmt input/, output/ und eigene Templates und startet die neue EXE. Die alte Installation bleibt als Rückfallebene erhalten; eine angepasste settings.ini wird als settings.vorherige-version.ini aufbewahrt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e2fab7fd3
commit
cb059ac20d
7 files changed
+315
-5
No files matched your search
@@ -0,0 +1,121 @@
|
||||
"""Tests für das Einrichten einer neuen Version neben der aktuellen Installation."""
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
import update_installer
|
||||
from update_installer import EXE_NAME, UpdateError, install_update
|
||||
|
||||
|
||||
class InstallUpdateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._temporary = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self._temporary.name).resolve()
|
||||
self.current = self.root / "Tätigkeitsnachweis-Generator v1.3.3"
|
||||
for folder in ["input", "output/2026", "config", "template"]:
|
||||
(self.current / folder).mkdir(parents=True)
|
||||
(self.current / EXE_NAME).write_bytes(b"old")
|
||||
(self.current / "input" / "daten.xlsx").write_bytes(b"input")
|
||||
(self.current / "output" / "2026" / "nachweis.pdf").write_bytes(b"output")
|
||||
(self.current / "config" / "settings.ini").write_bytes(b"[a]\nx=1\n")
|
||||
(self.current / "template" / "vorlage.pdf").write_bytes(b"old template")
|
||||
(self.current / "template" / "eigene.pdf").write_bytes(b"own template")
|
||||
self.share = self.root / "share"
|
||||
self.share.mkdir()
|
||||
|
||||
def tearDown(self):
|
||||
self._temporary.cleanup()
|
||||
|
||||
def make_zip(self, files=None, version="1.4.0"):
|
||||
files = files if files is not None else {
|
||||
EXE_NAME: b"new",
|
||||
"config/settings.ini": b"[a]\nx=1\n",
|
||||
"template/vorlage.pdf": b"new template",
|
||||
"input/": b"", "output/": b"",
|
||||
}
|
||||
path = self.share / f"Tätigkeitsnachweis-Generator v{version}.zip"
|
||||
with zipfile.ZipFile(path, "w") as package:
|
||||
for name, content in files.items():
|
||||
package.writestr(name, content)
|
||||
return path
|
||||
|
||||
def test_installs_next_to_current_and_copies_user_data(self):
|
||||
result = install_update(self.make_zip(), self.current)
|
||||
target = self.root / "Tätigkeitsnachweis-Generator v1.4.0"
|
||||
self.assertEqual(result.target_dir, target)
|
||||
self.assertFalse(result.already_installed)
|
||||
self.assertIsNone(result.previous_settings)
|
||||
self.assertEqual((target / EXE_NAME).read_bytes(), b"new")
|
||||
self.assertEqual((target / "input" / "daten.xlsx").read_bytes(), b"input")
|
||||
self.assertEqual((target / "output" / "2026" / "nachweis.pdf").read_bytes(), b"output")
|
||||
self.assertEqual((target / "template" / "vorlage.pdf").read_bytes(), b"new template")
|
||||
self.assertEqual((target / "template" / "eigene.pdf").read_bytes(), b"own template")
|
||||
# Die alte Installation bleibt unverändert.
|
||||
self.assertEqual((self.current / EXE_NAME).read_bytes(), b"old")
|
||||
self.assertEqual([p.name for p in self.root.iterdir() if p.name.startswith(".")], [])
|
||||
|
||||
def test_keeps_changed_settings_next_to_new_ones(self):
|
||||
(self.current / "config" / "settings.ini").write_text("[a]\nx=2\n", encoding="utf-8")
|
||||
result = install_update(self.make_zip(), self.current)
|
||||
config = result.target_dir / "config"
|
||||
self.assertEqual((config / "settings.ini").read_text(encoding="utf-8"), "[a]\nx=1\n")
|
||||
self.assertEqual(result.previous_settings, config / "settings.vorherige-version.ini")
|
||||
self.assertEqual(result.previous_settings.read_text(encoding="utf-8"), "[a]\nx=2\n")
|
||||
|
||||
def test_existing_installation_is_only_reused(self):
|
||||
zip_path = self.make_zip()
|
||||
install_update(zip_path, self.current)
|
||||
result = install_update(zip_path, self.current)
|
||||
self.assertTrue(result.already_installed)
|
||||
|
||||
def test_rejects_package_without_exe(self):
|
||||
with self.assertRaisesRegex(UpdateError, "keine"):
|
||||
install_update(self.make_zip({"README.md": b"x"}), self.current)
|
||||
self.assertFalse((self.root / "Tätigkeitsnachweis-Generator v1.4.0").exists())
|
||||
|
||||
def test_rejects_path_traversal(self):
|
||||
with self.assertRaisesRegex(UpdateError, "ungültigen Pfad"):
|
||||
install_update(self.make_zip({EXE_NAME: b"x", "../evil.txt": b"x"}), self.current)
|
||||
self.assertFalse((self.root / "evil.txt").exists())
|
||||
|
||||
def test_rejects_broken_zip_and_cleans_up(self):
|
||||
broken = self.share / "Tätigkeitsnachweis-Generator v1.4.0.zip"
|
||||
broken.write_bytes(b"no zip")
|
||||
with self.assertRaisesRegex(UpdateError, "keine gültige ZIP"):
|
||||
install_update(broken, self.current)
|
||||
self.assertEqual(sorted(p.name for p in self.root.iterdir()), sorted([self.current.name, "share"]))
|
||||
|
||||
def test_unreachable_share_gives_vpn_hint(self):
|
||||
with self.assertRaisesRegex(UpdateError, "VPN"):
|
||||
install_update(self.share / "Tätigkeitsnachweis-Generator v1.4.0.zip", self.current)
|
||||
|
||||
def test_refuses_existing_folder_without_program(self):
|
||||
(self.root / "Tätigkeitsnachweis-Generator v1.4.0").mkdir()
|
||||
with self.assertRaisesRegex(UpdateError, "existiert bereits"):
|
||||
install_update(self.make_zip(), self.current)
|
||||
|
||||
|
||||
class StartApplicationTests(unittest.TestCase):
|
||||
@unittest.skipUnless(sys.platform == "win32", "Windows-Prozessflags")
|
||||
def test_starts_new_exe_with_clean_bundle_environment(self):
|
||||
target = Path("C:/neu")
|
||||
environment = {"PATH": "x", "APP_BASE_DIR": "alt", "_MEIPASS2": "alt",
|
||||
"_PYI_APPLICATION_HOME_DIR": "alt", "STREAMLIT_SERVER_PORT": "8501"}
|
||||
with patch.dict(update_installer.os.environ, environment, clear=True), \
|
||||
patch.object(update_installer.subprocess, "Popen") as popen:
|
||||
update_installer.start_application(target)
|
||||
args, kwargs = popen.call_args
|
||||
self.assertEqual(args[0], [str(target / EXE_NAME)])
|
||||
self.assertEqual(kwargs["cwd"], target)
|
||||
self.assertEqual(kwargs["env"], {"PATH": "x", "PYINSTALLER_RESET_ENVIRONMENT": "1"})
|
||||
self.assertTrue(kwargs["creationflags"] & subprocess.CREATE_NEW_CONSOLE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user