172 lines
8.5 KiB
Python
172 lines
8.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests: Startpanel Praxis Chat Office-SSO (Chat-Office-Identity, Block B).
|
|
|
|
Deckt ab: laufendes Office (IPC), Token-Handoff bei Profil, frische Installation
|
|
(Einrichtungsfenster), bewusster Logout, ungueltiger Token, Shell-Session-Endpunkt,
|
|
Packaging-Marker.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import aza_start_panel as sp
|
|
|
|
|
|
class TestStartpanelChatOfficeSso(unittest.TestCase):
|
|
# --- Prioritaet 1: laufendes Office ---
|
|
def test_office_running_uses_ipc(self) -> None:
|
|
with patch.object(sp, "_startpanel_office_running", return_value=True), patch(
|
|
"aza_empfang_shell_surface.request_office_open_empfang_chat_shell_ipc"
|
|
) as ipc, patch.object(sp, "_launch_praxischat_process") as launch:
|
|
ok, msg = sp.start_praxischat()
|
|
self.assertTrue(ok)
|
|
ipc.assert_called_once()
|
|
launch.assert_not_called()
|
|
self.assertIn("Office", msg)
|
|
|
|
def test_office_minimized_still_ipc(self) -> None:
|
|
# Office minimiert wird via tasklist genauso als laufend erkannt -> IPC, kein Restore-Zwang.
|
|
with patch.object(sp, "_startpanel_office_running", return_value=True), patch(
|
|
"aza_empfang_shell_surface.request_office_open_empfang_chat_shell_ipc"
|
|
) as ipc, patch.object(sp, "_launch_praxischat_process") as launch:
|
|
ok, _msg = sp.start_praxischat()
|
|
self.assertTrue(ok)
|
|
ipc.assert_called_once()
|
|
launch.assert_not_called()
|
|
|
|
# --- Prioritaet 2: Office aus, Profil vorhanden ---
|
|
def test_profile_token_handoff(self) -> None:
|
|
with patch.object(sp, "_startpanel_office_running", return_value=False), patch.object(
|
|
sp.aza_persistence, "load_user_profile",
|
|
return_value={"practice_id": "p1", "empfang_user_id": "u1"},
|
|
), patch.object(sp, "_startpanel_logout_marker_active", return_value=False), patch.object(
|
|
sp, "_startpanel_fetch_shell_token", return_value="tok-abc"
|
|
), patch.object(sp, "_launch_praxischat_process", return_value=(True, "ok")) as launch:
|
|
ok, _msg = sp.start_praxischat()
|
|
self.assertTrue(ok)
|
|
launch.assert_called_once_with(["--handoff-token=tok-abc"])
|
|
|
|
def test_invalid_token_safe_fallback(self) -> None:
|
|
# Profil vorhanden, aber Token-Fetch schlaegt fehl -> bare Huelle (kein Loop, kein Flacker).
|
|
with patch.object(sp, "_startpanel_office_running", return_value=False), patch.object(
|
|
sp.aza_persistence, "load_user_profile",
|
|
return_value={"practice_id": "p1", "empfang_user_id": "u1"},
|
|
), patch.object(sp, "_startpanel_logout_marker_active", return_value=False), patch.object(
|
|
sp, "_startpanel_fetch_shell_token", return_value=None
|
|
), patch.object(sp, "_launch_praxischat_process", return_value=(True, "bare")) as launch:
|
|
ok, msg = sp.start_praxischat()
|
|
self.assertTrue(ok)
|
|
launch.assert_called_once_with()
|
|
self.assertEqual(msg, "bare")
|
|
|
|
# --- bewusster Logout ---
|
|
def test_logout_respected_no_autologin(self) -> None:
|
|
# Profil vorhanden, aber bewusster Logout -> KEIN Token-Fetch, bare Huelle.
|
|
with patch.object(sp, "_startpanel_office_running", return_value=False), patch.object(
|
|
sp.aza_persistence, "load_user_profile",
|
|
return_value={"practice_id": "p1", "empfang_user_id": "u1"},
|
|
), patch.object(sp, "_startpanel_logout_marker_active", return_value=True), patch.object(
|
|
sp, "_startpanel_fetch_shell_token"
|
|
) as fetch, patch.object(sp, "_launch_praxischat_process", return_value=(True, "bare")) as launch:
|
|
ok, _msg = sp.start_praxischat()
|
|
self.assertTrue(ok)
|
|
fetch.assert_not_called()
|
|
launch.assert_called_once_with()
|
|
|
|
# --- Prioritaet 3: frische Installation ohne Identitaet ---
|
|
def test_no_identity_shows_setup_window(self) -> None:
|
|
with patch.object(sp, "_startpanel_office_running", return_value=False), patch.object(
|
|
sp.aza_persistence, "load_user_profile", return_value={}
|
|
), patch.object(sp, "_startpanel_fetch_shell_token") as fetch, patch.object(
|
|
sp, "_launch_praxischat_process"
|
|
) as launch, patch.object(sp, "_show_office_setup_required_window", return_value=True) as setup:
|
|
ok, msg = sp.start_praxischat()
|
|
self.assertTrue(ok)
|
|
setup.assert_called_once()
|
|
fetch.assert_not_called()
|
|
launch.assert_not_called()
|
|
self.assertIn("eingerichtet", msg)
|
|
|
|
def test_no_identity_no_default_practice(self) -> None:
|
|
# Kein stiller Auto-Login / keine Default-Praxis bei fehlender Identitaet.
|
|
with patch.object(sp, "_startpanel_office_running", return_value=False), patch.object(
|
|
sp.aza_persistence, "load_user_profile", return_value={"name": "Nur Name"}
|
|
), patch.object(sp, "_startpanel_fetch_shell_token") as fetch, patch.object(
|
|
sp, "_show_office_setup_required_window", return_value=True
|
|
):
|
|
sp.start_praxischat()
|
|
fetch.assert_not_called()
|
|
|
|
# --- Shell-Session-Endpunkt ---
|
|
def test_fetch_shell_token_requires_practice_and_user(self) -> None:
|
|
with patch.object(sp.aza_persistence, "load_user_profile", return_value={"practice_id": "p1"}):
|
|
self.assertIsNone(sp._startpanel_fetch_shell_token())
|
|
|
|
def test_fetch_shell_token_success(self) -> None:
|
|
resp = MagicMock(status_code=200)
|
|
resp.json.return_value = {"shell_token": "shell-xyz"}
|
|
with patch.object(
|
|
sp.aza_persistence, "load_user_profile",
|
|
return_value={"practice_id": "p1", "empfang_user_id": "u1"},
|
|
), patch.object(sp, "_startpanel_backend_url", return_value="https://api.test"), patch.object(
|
|
sp, "_startpanel_backend_token", return_value="api-tok"
|
|
), patch("aza_start_panel.requests.post", return_value=resp) as post:
|
|
tok = sp._startpanel_fetch_shell_token()
|
|
self.assertEqual(tok, "shell-xyz")
|
|
post.assert_called_once()
|
|
_args, kwargs = post.call_args
|
|
self.assertIn("/empfang/shell/session", _args[0])
|
|
hdrs = kwargs["headers"]
|
|
self.assertEqual(hdrs["X-Practice-Id"], "p1")
|
|
self.assertEqual(hdrs["X-AzA-Empfang-User-Id"], "u1")
|
|
# Kein Passwort in den Headers/Payload.
|
|
self.assertNotIn("password", {k.lower(): v for k, v in hdrs.items()})
|
|
self.assertEqual(kwargs.get("json"), {})
|
|
|
|
def test_fetch_shell_token_server_error_returns_none(self) -> None:
|
|
resp = MagicMock(status_code=403)
|
|
with patch.object(
|
|
sp.aza_persistence, "load_user_profile",
|
|
return_value={"practice_id": "p1", "empfang_user_id": "u1"},
|
|
), patch.object(sp, "_startpanel_backend_url", return_value="https://api.test"), patch.object(
|
|
sp, "_startpanel_backend_token", return_value="api-tok"
|
|
), patch("aza_start_panel.requests.post", return_value=resp):
|
|
self.assertIsNone(sp._startpanel_fetch_shell_token())
|
|
|
|
# --- Handoff verwendet kein URL/Log-Leak: Token nur als argv ---
|
|
def test_handoff_token_passed_as_process_arg(self) -> None:
|
|
captured = {}
|
|
|
|
def _fake_popen(args, **kwargs):
|
|
captured["args"] = args
|
|
return MagicMock()
|
|
|
|
with patch.object(sp, "_resolve_empfang_shell_executable", return_value=Path("X/AZA_EmpfangShell.exe")), \
|
|
patch("aza_start_panel.subprocess.Popen", side_effect=_fake_popen):
|
|
sp._launch_praxischat_process(["--handoff-token=secret-tok"])
|
|
# Token als eigenes argv-Element, nicht in der URL.
|
|
self.assertIn("--handoff-token=secret-tok", captured["args"])
|
|
url_args = [a for a in captured["args"] if str(a).startswith("http")]
|
|
for u in url_args:
|
|
self.assertNotIn("secret-tok", u)
|
|
|
|
# --- Source-/Packaging-Marker ---
|
|
def test_source_contains_ipc_and_handoff(self) -> None:
|
|
src = Path(sp.__file__).read_text(encoding="utf-8")
|
|
self.assertIn("request_office_open_empfang_chat_shell_ipc", src)
|
|
self.assertIn("--handoff-token=", src)
|
|
self.assertIn("_startpanel_fetch_shell_token", src)
|
|
self.assertIn("/empfang/shell/session", src)
|
|
self.assertIn("_show_office_setup_required_window", src)
|
|
|
|
def test_no_password_handling_in_source(self) -> None:
|
|
src = Path(sp.__file__).read_text(encoding="utf-8")
|
|
# Kein Passwort-Lesen/-Weitergeben im Startpanel-SSO.
|
|
self.assertNotIn("password=", src.lower())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|