255 lines
11 KiB
Python
255 lines
11 KiB
Python
# -*- 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()
|