test_public_server_coverage.py 6.2 KB

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