120 lines
6.8 KiB
Python
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) |