"""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()