# -*- coding: utf-8 -*- """Hotfix 1.3.15: Hauptfenster zuerst + Aktivierungsdialog haengt nicht bei "Wird geprueft...". Kein Tk-Display noetig: Struktur-/Quelltextpruefungen + verhaltensbasierte Aufrufe der real gebundenen Methoden auf einem Stub (wie _test_license_first_start_blocker.py). """ from __future__ import annotations import inspect import threading import unittest from unittest.mock import patch import basis14 from basis14 import KGDesktopApp def _src(fn) -> str: return inspect.getsource(fn) class StartupNotBlockingTests(unittest.TestCase): def test_init_does_not_call_check_license_status_synchronously(self): src = _src(KGDesktopApp.__init__) self.assertNotIn( "self.check_license_status()", src, "check_license_status() darf NICHT mehr synchron im __init__ laufen", ) self.assertIn('self.license_mode = "demo"', src) self.assertIn("self._startup_license_check_started = False", src) def test_ensure_centered_kicks_license_check_after_deiconify(self): # FIX First-Start (2026-06-17): _ensure_centered stoesst nach deiconify die # Post-visible-Startup-Sequenz an; diese ruft die Lizenzpruefung als letzten Schritt. src = _src(KGDesktopApp._ensure_centered) deic = src.find("self.deiconify()") kick = src.find("_run_post_visible_startup_sequence") self.assertGreater(deic, 0, "deiconify fehlt") self.assertGreater(kick, 0, "Post-visible-Startup-Sequenz wird nicht angestossen") self.assertLess(deic, kick, "Startup-Sequenz muss NACH deiconify angestossen werden") seq = _src(KGDesktopApp._run_post_visible_startup_sequence) self.assertIn("_start_startup_license_check", seq, "Sequenz muss die Lizenzpruefung anstossen") def test_startup_check_runs_in_worker_thread(self): src = _src(KGDesktopApp._start_startup_license_check) self.assertIn("check_license_status", src) self.assertIn("threading.Thread", src) self.assertIn("_startup_license_check_started", src) def test_startup_check_guard_prevents_double_start(self): started = {"n": 0} class _T: def __init__(self, *a, **k): pass def start(self_inner): started["n"] += 1 app = type("A", (), {})() app._startup_license_check_started = False app.check_license_status = lambda: None app.after = lambda *a, **k: None with patch("basis14.threading.Thread", _T): KGDesktopApp._start_startup_license_check(app) KGDesktopApp._start_startup_license_check(app) self.assertEqual(started["n"], 1, "Nur eine Lizenzpruefung darf gestartet werden") class PostVisibleStartupSequenceTests(unittest.TestCase): """FIX First-Start (2026-06-17): Fachgebiet/Consent/Lizenz sequenziell nach sichtbarem Fenster.""" def test_init_does_not_open_specialty_or_consent_dialog(self): src = _src(KGDesktopApp.__init__) self.assertNotIn( "self._ensure_user_specialty_preferences()", src, "Fachgebietsdialog darf NICHT mehr synchron im __init__ laufen", ) self.assertNotIn( "self._show_consent_dialog()", src, "Consent-Dialog darf NICHT mehr synchron im __init__ laufen", ) def test_sequence_order_specialty_then_consent_then_license(self): src = _src(KGDesktopApp._run_post_visible_startup_sequence) i_spec = src.find("_ensure_user_specialty_preferences") i_consent = src.find("_show_consent_dialog") i_lic = src.find("_start_startup_license_check") self.assertGreater(i_spec, 0, "Fachgebiet fehlt in Sequenz") self.assertGreater(i_consent, 0, "Consent fehlt in Sequenz") self.assertGreater(i_lic, 0, "Lizenzpruefung fehlt in Sequenz") self.assertLess(i_spec, i_consent, "Fachgebiet muss vor Consent laufen") self.assertLess(i_consent, i_lic, "Consent muss vor Lizenzpruefung laufen") def test_consent_condition_preserved(self): src = _src(KGDesktopApp._run_post_visible_startup_sequence) self.assertIn("has_valid_consent", src, "Bestehende Consent-Bedingung muss erhalten bleiben") def test_sequence_runs_exactly_once(self): order = [] app = type("A", (), {})() app._user_profile = {"name": "Tester"} app._ensure_user_specialty_preferences = lambda: order.append("spec") app._show_consent_dialog = lambda: order.append("consent") app._start_startup_license_check = lambda: order.append("lic") with patch("basis14.has_valid_consent", return_value=False): KGDesktopApp._run_post_visible_startup_sequence(app) KGDesktopApp._run_post_visible_startup_sequence(app) self.assertEqual(order, ["spec", "consent", "lic"], "Sequenz muss genau einmal in korrekter Reihenfolge laufen") self.assertTrue(app._post_visible_startup_done) def test_consent_skipped_when_valid(self): order = [] app = type("A", (), {})() app._user_profile = {"name": "Tester"} app._ensure_user_specialty_preferences = lambda: order.append("spec") app._show_consent_dialog = lambda: order.append("consent") app._start_startup_license_check = lambda: order.append("lic") with patch("basis14.has_valid_consent", return_value=True): KGDesktopApp._run_post_visible_startup_sequence(app) self.assertEqual(order, ["spec", "lic"], "Bei gueltigem Consent darf der Consent-Dialog nicht erscheinen") def test_specialty_internal_guard_unchanged(self): # Fachlogik-Guard 'user_specialties_selected' bleibt in der Dialogfunktion selbst. src = _src(KGDesktopApp._ensure_user_specialty_preferences) self.assertIn('user_specialties_selected', src) def test_license_failure_does_not_block_sequence(self): order = [] app = type("A", (), {})() app._user_profile = {"name": "Tester"} app._ensure_user_specialty_preferences = lambda: order.append("spec") def _boom(): order.append("consent_boom") raise RuntimeError("consent fail") app._show_consent_dialog = _boom app._start_startup_license_check = lambda: order.append("lic") with patch("basis14.has_valid_consent", return_value=False): KGDesktopApp._run_post_visible_startup_sequence(app) # Trotz Exception im Consent muss die Lizenzpruefung danach starten. self.assertIn("lic", order, "Lizenzpruefung muss auch nach Dialogfehler starten") class AfterCheckRoutingTests(unittest.TestCase): def _stub(self, license_mode="demo", pending=None, backend="https://api.aza-medwork.ch"): calls = [] app = type("A", (), {})() app.license_mode = license_mode if pending is not None: app._pending_device_limit_msg = pending app.get_backend_url = lambda: backend app._update_license_indicator = lambda: calls.append(("indicator",)) app._show_device_limit_notice = lambda: calls.append(("device_limit",)) app._show_license_required_notice = lambda: calls.append(("license_required",)) def _after(delay, fn, *a): # Routing nutzt after(0, handler) -> sofort ausfuehren, um Ziel zu erfassen. calls.append(("after", delay)) try: fn(*a) except Exception: pass app.after = _after return app, calls def test_demo_remote_no_pending_routes_to_license_required(self): app, calls = self._stub(license_mode="demo", pending=None) KGDesktopApp._after_startup_license_check(app) targets = [c[0] for c in calls] self.assertIn("license_required", targets) self.assertNotIn("device_limit", targets) def test_demo_remote_pending_routes_to_device_limit(self): app, calls = self._stub(license_mode="demo", pending="Geraete-Limit erreicht.") KGDesktopApp._after_startup_license_check(app) targets = [c[0] for c in calls] self.assertIn("device_limit", targets) self.assertNotIn("license_required", targets) def test_active_mode_shows_no_activation_dialog(self): app, calls = self._stub(license_mode="active") KGDesktopApp._after_startup_license_check(app) targets = [c[0] for c in calls] self.assertNotIn("license_required", targets) self.assertNotIn("device_limit", targets) self.assertIn("indicator", targets) def test_offline_backend_uses_activation_key_fallback(self): app, calls = self._stub(license_mode="demo", backend="http://127.0.0.1:8000") with patch("aza_activation.load_activation_key", return_value="AZA-KEY"), \ patch("aza_activation.validate_key", return_value=(True, 9999999999, "ok")): KGDesktopApp._after_startup_license_check(app) self.assertEqual(app.license_mode, "active") targets = [c[0] for c in calls] self.assertNotIn("license_required", targets) class ActivationDialogHangTests(unittest.TestCase): """Quelltextpruefung des Aktivierungs-Workers (Tk-Dialog nicht headless instanziierbar).""" def setUp(self): self.src = _src(KGDesktopApp._show_activation_dialog) def test_worker_has_catch_all_and_finally_reset(self): self.assertIn("except requests.RequestException", self.src) self.assertIn("except Exception as e:", self.src) self.assertIn("finally:", self.src) self.assertIn('_act_state["checking"] = False', self.src) self.assertIn("_set_activate_enabled(True)", self.src) def test_multi_click_guard(self): self.assertIn('if _act_state["checking"]:', self.src) # Guard steht vor dem Setzen von checking=True guard = self.src.find('if _act_state["checking"]:') set_true = self.src.find('_act_state["checking"] = True') self.assertGreater(guard, 0) self.assertGreater(set_true, guard) def test_status_updates_guarded_against_destroyed_dialog(self): self.assertIn("_safe_status", self.src) self.assertIn('if not _act_state["alive"]:', self.src) def test_demo_continue_releases_and_shows_main_window(self): # Demo/Spaeter/X teilen sich _demo_continue block = self.src[self.src.find("def _demo_continue("):] for needed in ( '_act_state["alive"] = False', "dlg.grab_release()", "dlg.destroy()", "self.deiconify()", "self.lift()", "self.focus_set()", ): self.assertIn(needed, block, f"_demo_continue fehlt: {needed}") self.assertIn('dlg.protocol("WM_DELETE_WINDOW", _demo_continue)', self.src) def test_activate_has_timeout(self): self.assertIn("timeout=10", self.src) def test_no_secret_logging(self): # Keine Klartext-Tokens/Keys in neuen Statusmeldungen self.assertNotIn("api_token}", self.src) self.assertNotIn("X-API-Token: ", self.src) if __name__ == "__main__": unittest.main()