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.')