test_public_server_coverage.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """
  2. Coverage补充测试:public_server.py
  3. 目标路径:
  4. - extract_client_ip — 空 client_address
  5. - do_GET — 非 /health 路径 → 404
  6. - do_POST — Content-Type 正确但 body 为无效 JSON → -32700 parse error
  7. - PublicMcpHttpHandler.handle_json_rpc — 未知 method → -32601 error
  8. """
  9. import json
  10. import socket
  11. import threading
  12. import unittest
  13. from http.server import HTTPServer
  14. from public_server import PublicMcpHttpHandler, create_http_handler, extract_client_ip
  15. # ---------------------------------------------------------------------------
  16. # extract_client_ip 边界情况
  17. # ---------------------------------------------------------------------------
  18. class ExtractClientIpTest(unittest.TestCase):
  19. def test_returns_empty_string_when_client_address_is_none(self):
  20. result = extract_client_ip({}, None)
  21. self.assertEqual('', result)
  22. def test_returns_empty_string_when_client_address_is_empty_tuple(self):
  23. result = extract_client_ip({}, ())
  24. self.assertEqual('', result)
  25. def test_returns_ip_from_client_address(self):
  26. result = extract_client_ip({'X-Forwarded-For': '1.2.3.4'}, ('10.0.0.1', 12345))
  27. self.assertEqual('10.0.0.1', result)
  28. # ---------------------------------------------------------------------------
  29. # PublicMcpHttpHandler.handle_json_rpc — 未知 method
  30. # ---------------------------------------------------------------------------
  31. class FakeGateway:
  32. def registered_tool_names(self):
  33. return ('query_order',)
  34. def list_tools(self, gateway_session_id):
  35. return [{'name': 'query_order', 'description': '', 'input_schema': {'type': 'object'}}]
  36. def call_tool(self, *args, **kwargs):
  37. return {'data': {}}
  38. class HandleJsonRpcUnknownMethodTest(unittest.TestCase):
  39. def test_unknown_method_returns_method_not_found(self):
  40. handler = PublicMcpHttpHandler(FakeGateway())
  41. response = handler.handle_json_rpc(
  42. headers={},
  43. message={'jsonrpc': '2.0', 'id': 1, 'method': 'unknown/method'},
  44. )
  45. self.assertIn('error', response)
  46. self.assertEqual(-32601, response['error']['code'])
  47. # ---------------------------------------------------------------------------
  48. # 集成测试:启动真实 HTTP 服务器,覆盖 do_GET 404 和 do_POST JSON 错误
  49. # ---------------------------------------------------------------------------
  50. def _find_free_port():
  51. with socket.socket() as s:
  52. s.bind(('127.0.0.1', 0))
  53. return s.getsockname()[1]
  54. class HttpHandlerIntegrationTest(unittest.TestCase):
  55. """
  56. 启动一个真实的 ThreadingHTTPServer(随机端口),测试
  57. HTTP 层面的边界路径。
  58. """
  59. @classmethod
  60. def setUpClass(cls):
  61. port = _find_free_port()
  62. gateway = FakeGateway()
  63. handler_class = create_http_handler(gateway, rate_limiter=None)
  64. cls.server = HTTPServer(('127.0.0.1', port), handler_class)
  65. cls.server_thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
  66. cls.server_thread.start()
  67. cls.base_url = 'http://127.0.0.1:{0}'.format(port)
  68. @classmethod
  69. def tearDownClass(cls):
  70. cls.server.shutdown()
  71. def _get(self, path):
  72. import urllib.request
  73. req = urllib.request.Request(cls_url := self.base_url + path, method='GET')
  74. try:
  75. with urllib.request.urlopen(req, timeout=5) as resp:
  76. return resp.status, resp.read()
  77. except Exception as e:
  78. # urllib raises HTTPError for non-2xx
  79. if hasattr(e, 'code'):
  80. return e.code, b''
  81. raise
  82. def _post(self, path, body, content_type='application/json'):
  83. import urllib.request
  84. data = body.encode('utf-8') if isinstance(body, str) else body
  85. req = urllib.request.Request(
  86. self.base_url + path,
  87. data=data,
  88. headers={'Content-Type': content_type, 'Content-Length': str(len(data))},
  89. method='POST',
  90. )
  91. try:
  92. with urllib.request.urlopen(req, timeout=5) as resp:
  93. return resp.status, json.loads(resp.read())
  94. except Exception as e:
  95. if hasattr(e, 'code') and hasattr(e, 'read'):
  96. body_bytes = e.read()
  97. return e.code, json.loads(body_bytes) if body_bytes else {}
  98. raise
  99. # do_GET /health → 200
  100. def test_get_health_returns_200(self):
  101. status, body = self._get('/health')
  102. self.assertEqual(200, status)
  103. # do_GET 非 /health → 404(覆盖 lines 113-114)
  104. def test_get_unknown_path_returns_404(self):
  105. import urllib.request
  106. req = urllib.request.Request(self.base_url + '/not-a-real-path', method='GET')
  107. try:
  108. urllib.request.urlopen(req, timeout=5)
  109. self.fail('Expected 404')
  110. except Exception as e:
  111. self.assertEqual(404, e.code)
  112. # do_POST 无效 JSON → -32700 parse error(覆盖 lines 127-130)
  113. def test_post_invalid_json_returns_parse_error(self):
  114. status, body = self._post('/mcp', 'this is not json at all')
  115. self.assertEqual(200, status)
  116. self.assertIn('error', body)
  117. self.assertEqual(-32700, body['error']['code'])
  118. self.assertEqual('Parse error', body['error']['message'])
  119. # do_POST 到非 /mcp 路径 → 404
  120. def test_post_wrong_path_returns_404(self):
  121. import urllib.request
  122. data = json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': 'initialize'}).encode()
  123. req = urllib.request.Request(
  124. self.base_url + '/wrong',
  125. data=data,
  126. headers={'Content-Type': 'application/json', 'Content-Length': str(len(data))},
  127. method='POST',
  128. )
  129. try:
  130. urllib.request.urlopen(req, timeout=5)
  131. self.fail('Expected 404')
  132. except Exception as e:
  133. self.assertEqual(404, e.code)
  134. # do_POST 正常 initialize → 200
  135. def test_post_initialize_returns_200(self):
  136. status, body = self._post('/mcp', json.dumps({
  137. 'jsonrpc': '2.0',
  138. 'id': 1,
  139. 'method': 'initialize',
  140. 'params': {},
  141. }))
  142. self.assertEqual(200, status)
  143. self.assertIn('result', body)
  144. self.assertIn('protocolVersion', body['result'])
  145. if __name__ == '__main__':
  146. unittest.main()