Files
kefu/wechat_rpa/test_patient_session_dialog.py
T
2026-09-21 10:34:06 +08:00

120 lines
6.8 KiB
Python

"""Isolated patient-auth UI regressions; no live patient lookups or messages."""
from unittest import TestCase, mock
from types import SimpleNamespace
from PySide6.QtWidgets import QApplication, QDialog
import backend_client as backend
import wechat_gui_qt as qt
CONTEXT = {'permissions': {'can_search': True, 'can_bind': True, 'can_unbind': True},
'person': {'id': 'person-test', 'display_name': '测试联系人'},
'people': [{'id': 'person-test', 'display_name': '测试联系人'}]}
RESULT = {'items': [{'patient_id': 42, 'patient_name': '测试患者', 'phone_masked': '13****01'}], 'total': 1}
class ImmediateThread:
def __init__(self, *, target, **kwargs): self.target = target
def start(self): self.target()
class PatientSessionDialogTest(TestCase):
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def setUp(self):
self.dialog = qt.PatientBindingDialog(CONTEXT, 'account-test', 'conversation-test')
self.addCleanup(self.dialog.deleteLater)
self.addCleanup(self.dialog.reject)
self.dialog.show()
self.dialog.search_input.setText('测试')
self.app.processEvents()
patcher = mock.patch.object(qt.threading, 'Thread', ImmediateThread)
patcher.start(); self.addCleanup(patcher.stop)
def expire(self):
with mock.patch.object(backend, 'search_desktop_patients', side_effect=backend.PatientSessionExpired('会话过期')):
self.dialog.search_button.click()
self.app.processEvents()
def test_expired_search_offers_reauth_clears_stale_results_and_blocks_binding(self):
self.dialog._search_finished({'ok': True, 'result': RESULT})
self.assertEqual(self.dialog.result_table.rowCount(), 1)
self.expire()
self.assertTrue(self.dialog.reauthenticate_button.isVisible())
self.assertFalse(self.dialog.bind_button.isEnabled())
self.assertFalse(self.dialog.search_button.isEnabled())
self.assertEqual(self.dialog.result_table.rowCount(), 0)
self.assertIn('客服监听继续运行', self.dialog.hint_label.text())
self.assertFalse(self.dialog.changed)
def test_network_error_does_not_request_reauthentication(self):
with mock.patch.object(backend, 'search_desktop_patients', side_effect=backend.BackendError('连接超时')):
self.dialog.search_button.click()
self.assertFalse(self.dialog.reauthenticate_button.isVisible())
self.assertTrue(self.dialog.search_button.isEnabled())
self.assertIn('连接超时', self.dialog.hint_label.text())
def test_cancel_reauthentication_leaves_dialog_open_and_calls_no_service(self):
self.expire()
with mock.patch.object(backend, 'load_settings', return_value={'zyt_account': 'test-user'}), \
mock.patch.object(qt.QInputDialog, 'getText', return_value=('', False)), \
mock.patch.object(backend, 'reauthenticate_patient_session') as reauth:
self.dialog.reauthenticate_button.click()
reauth.assert_not_called()
self.assertTrue(self.dialog.isVisible())
self.assertTrue(self.dialog.reauthenticate_button.isEnabled())
def test_reauthentication_reloads_permissions_and_retries_only_search_once(self):
self.expire()
with mock.patch.object(backend, 'load_settings', return_value={'zyt_account': 'test-user'}), \
mock.patch.object(qt.QInputDialog, 'getText', return_value=('synthetic-password', True)), \
mock.patch.object(backend, 'reauthenticate_patient_session', return_value={'ok': True}) as reauth, \
mock.patch.object(backend, 'desktop_patient_context', return_value=CONTEXT) as context, \
mock.patch.object(backend, 'search_desktop_patients', return_value=RESULT) as search, \
mock.patch.object(backend, 'bind_desktop_patient') as bind:
self.dialog.reauthenticate_button.click()
reauth.assert_called_once_with('synthetic-password')
context.assert_called_once_with('account-test', 'conversation-test', person_id='person-test')
search.assert_called_once_with('测试')
bind.assert_not_called()
self.assertFalse(self.dialog.reauthenticate_button.isVisible())
self.assertEqual(self.dialog.result_table.rowCount(), 1)
self.assertFalse(self.dialog.changed)
def test_reauthenticated_account_without_permission_does_not_search(self):
self.expire()
context = {**CONTEXT, 'permissions': {'can_search': False, 'can_bind': False, 'reason': '无患者权限'}}
with mock.patch.object(backend, 'search_desktop_patients') as search:
self.dialog._reauthentication_finished({'ok': True, 'context': context})
search.assert_not_called()
self.assertFalse(self.dialog.search_button.isEnabled())
self.assertFalse(self.dialog.bind_button.isEnabled())
self.assertIn('无患者权限', self.dialog.hint_label.text())
def test_reauthentication_failure_allows_retry_without_password_in_display(self):
self.expire()
with mock.patch.object(backend, 'load_settings', return_value={'zyt_account': 'test-user'}), \
mock.patch.object(qt.QInputDialog, 'getText', return_value=('synthetic-password', True)), \
mock.patch.object(backend, 'reauthenticate_patient_session', side_effect=backend.BackendError('密码错误')):
self.dialog.reauthenticate_button.click()
self.assertTrue(self.dialog.reauthenticate_button.isEnabled())
self.assertIn('密码错误', self.dialog.hint_label.text())
self.assertNotIn('synthetic-password', self.dialog.hint_label.text())
self.assertTrue(self.dialog.isVisible())
def test_expired_bind_never_retries_mutation_automatically(self):
self.dialog._search_finished({'ok': True, 'result': RESULT})
with mock.patch.object(backend, 'bind_desktop_patient', side_effect=backend.PatientSessionExpired('过期')) as bind:
self.dialog.bind_button.click()
bind.assert_called_once()
self.assertTrue(self.dialog.reauthenticate_button.isVisible())
self.assertFalse(self.dialog.changed)
def test_keepalive_expiry_is_a_local_warning_and_does_not_stop_monitor(self):
host = SimpleNamespace(_patient_keepalive_running=True, _patient_session_state='unknown',
append_log=mock.Mock(), stop_monitoring=mock.Mock(),
_handle_forced_desktop_logout=mock.Mock())
qt.MainWindow._patient_session_checked(host, {'status': 'expired'})
qt.MainWindow._patient_session_checked(host, {'status': 'expired'})
host.append_log.assert_called_once()
host.stop_monitoring.assert_not_called()
host._handle_forced_desktop_logout.assert_not_called()
self.assertFalse(host._patient_keepalive_running)