Files
aza/AzA march 2026/_test_startpanel_chat_office_sso.py
2026-06-18 13:47:45 +02:00

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