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>
122 lines
5.9 KiB
Python
122 lines
5.9 KiB
Python
"""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()
|