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