113 lines
6.1 KiB
Python
113 lines
6.1 KiB
Python
from pathlib import Path
|
|
import hashlib
|
|
import json
|
|
|
|
root = Path('C:/kefu/wechat_rpa')
|
|
path = root / 'admin_api.py'
|
|
source = path.read_text(encoding='utf-8')
|
|
assert 'import urllib.error\n' not in source
|
|
source = source.replace('import urllib.parse\n', 'import urllib.error\nimport urllib.parse\n', 1)
|
|
old = ''' except zyt_auth.ZytAuthError as exc:
|
|
raise HTTPException(status_code=409, detail={
|
|
"code": "zyt_session_expired",
|
|
"message": "ZYT 患者查询登录已过期,请重新验证账号。",
|
|
}) from exc
|
|
'''
|
|
new = ''' except zyt_auth.ZytAuthError as exc:
|
|
# The identity adapter preserves transport/JSON causes. An outage must
|
|
# not send an otherwise logged-in user into a reauthentication loop.
|
|
cause = exc.__cause__
|
|
http_code = cause.code if isinstance(cause, urllib.error.HTTPError) else None
|
|
if http_code == 403 or any(word in str(exc) for word in ("权限", "无权")):
|
|
raise HTTPException(status_code=403, detail="ZYT 账号无权验证患者查询身份,请联系管理员。") from exc
|
|
unavailable = (
|
|
(http_code is not None and http_code != 401)
|
|
or (http_code is None and isinstance(cause, (urllib.error.URLError, OSError, ValueError, TypeError)))
|
|
or any(word in str(exc) for word in ("无法连接", "无效数据", "身份信息不完整", "配置不正确", "尚未部署"))
|
|
)
|
|
if unavailable:
|
|
raise HTTPException(status_code=502, detail={
|
|
"code": "zyt_unavailable",
|
|
"message": "ZYT 身份服务暂不可用,请稍后重试。",
|
|
}) from exc
|
|
raise HTTPException(status_code=409, detail={
|
|
"code": "zyt_session_expired",
|
|
"message": "ZYT 患者查询登录已过期,请重新验证账号。",
|
|
}) from exc
|
|
'''
|
|
assert source.count(old) == 1
|
|
path.write_text(source.replace(old, new), encoding='utf-8', newline='')
|
|
|
|
path = root / 'test_zyt_session_recovery.py'
|
|
source = path.read_text(encoding='utf-8')
|
|
marker = ' def test_revoked_desktop_is_rejected_before_upstream_validation(self):\n'
|
|
tests = ''' def actual_verifier(self):
|
|
self.before_verify = lambda: zyt_auth.verify_desktop_token('fresh-zyt', api_url='https://audit.invalid')
|
|
|
|
def assert_unavailable_preserves_session(self, response, before):
|
|
self.assertEqual(response.status_code, 502, response.text)
|
|
self.assertEqual(response.json()['detail']['code'], 'zyt_unavailable')
|
|
self.assertEqual(self.snapshot(), before)
|
|
self.assertEqual(self.client.get('/api/v2/desktop/me', headers=self.headers).status_code, 200)
|
|
|
|
def test_identity_network_failure_is_unavailable_not_expiry(self):
|
|
self.actual_verifier()
|
|
before = self.snapshot()
|
|
for error in (urllib.error.URLError('synthetic connection failure'), TimeoutError('synthetic timeout')):
|
|
with self.subTest(error=type(error).__name__), mock.patch('urllib.request.urlopen', side_effect=error):
|
|
self.assert_unavailable_preserves_session(self.refresh(), before)
|
|
|
|
def test_identity_server_failure_is_unavailable_not_expiry(self):
|
|
self.actual_verifier()
|
|
before = self.snapshot()
|
|
for code in (500, 502, 503):
|
|
error = urllib.error.HTTPError('https://audit.invalid', code, 'Synthetic failure', {}, io.BytesIO())
|
|
with self.subTest(code=code), mock.patch('urllib.request.urlopen', side_effect=error):
|
|
self.assert_unavailable_preserves_session(self.refresh(), before)
|
|
|
|
def test_identity_invalid_response_is_unavailable_not_expiry(self):
|
|
self.actual_verifier()
|
|
before = self.snapshot()
|
|
for body in (b'not-json', b'[]', b'{"code":1,"data":{}}'):
|
|
response = mock.MagicMock()
|
|
response.__enter__.return_value.read.return_value = body
|
|
with self.subTest(body=body), mock.patch('urllib.request.urlopen', return_value=response):
|
|
self.assert_unavailable_preserves_session(self.refresh(), before)
|
|
|
|
def test_identity_http_401_and_expired_payload_are_recoverable(self):
|
|
self.actual_verifier()
|
|
before = self.snapshot()
|
|
error = urllib.error.HTTPError('https://audit.invalid', 401, 'Unauthorized', {}, io.BytesIO())
|
|
with mock.patch('urllib.request.urlopen', side_effect=error):
|
|
response = self.refresh()
|
|
self.assertEqual(response.status_code, 409, response.text)
|
|
self.assertEqual(response.json()['detail']['code'], 'zyt_session_expired')
|
|
upstream = mock.MagicMock()
|
|
upstream.__enter__.return_value.read.return_value = json.dumps({'code': 0, 'msg': '登录已失效,请重新登录'}).encode()
|
|
with mock.patch('urllib.request.urlopen', return_value=upstream):
|
|
response = self.refresh()
|
|
self.assertEqual(response.status_code, 409, response.text)
|
|
self.assertEqual(response.json()['detail']['code'], 'zyt_session_expired')
|
|
self.assertEqual(self.snapshot(), before)
|
|
|
|
def test_identity_permission_rejection_remains_403(self):
|
|
self.actual_verifier()
|
|
before = self.snapshot()
|
|
error = urllib.error.HTTPError('https://audit.invalid', 403, 'Forbidden', {}, io.BytesIO())
|
|
with mock.patch('urllib.request.urlopen', side_effect=error):
|
|
response = self.refresh()
|
|
self.assertEqual(response.status_code, 403, response.text)
|
|
self.assertIn('无权', response.json()['detail'])
|
|
self.assertEqual(self.snapshot(), before)
|
|
|
|
'''
|
|
assert source.count(marker) == 1
|
|
path.write_text(source.replace(marker, tests + marker), encoding='utf-8', newline='')
|
|
|
|
manifest_path = Path('C:/kefu/deploy/patient-session-20260918/backend-manifest.json')
|
|
manifest = json.loads(manifest_path.read_text(encoding='utf-8'))
|
|
for entry in manifest:
|
|
entry['after_sha256'] = hashlib.sha256(Path(entry['path']).read_bytes()).hexdigest()
|
|
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
print('Updated identity failure classification and 5 regression tests.')
|