test_scoped_api_client.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import unittest
  2. from unittest.mock import MagicMock
  3. from services.scoped_api_client import ScopedApiClient
  4. class TestScopedApiClient(unittest.TestCase):
  5. def setUp(self):
  6. self.mock_transport = MagicMock()
  7. self.client = ScopedApiClient(
  8. base_url='http://api.example.com',
  9. transport=self.mock_transport,
  10. timeout=15
  11. )
  12. def test_call_tool_success(self):
  13. token = 'MT_valid_token'
  14. tool_code = 'query_order'
  15. route_path = '/mcp/tools/query_order'
  16. payload = {'order_no': 'ABC123'}
  17. request_id = 'rq_test123'
  18. self.mock_transport.post_json.return_value = {
  19. 'code': '0',
  20. 'msg': 'success',
  21. 'data': {'order': 'details'}
  22. }
  23. result = self.client.call_tool(token, tool_code, route_path, payload, request_id)
  24. # Verify transport call
  25. self.mock_transport.post_json.assert_called_once_with(
  26. 'http://api.example.com/mcp/tools/query_order',
  27. payload,
  28. {
  29. 'Authorization': 'Bearer MT_valid_token',
  30. 'X-MCP-Tool-Code': 'query_order',
  31. 'X-Request-Id': 'rq_test123',
  32. },
  33. 15
  34. )
  35. # Verify result
  36. self.assertEqual(result['code'], '0')
  37. self.assertEqual(result['data']['order'], 'details')
  38. def test_call_tool_includes_client_ip_header_when_present(self):
  39. self.mock_transport.post_json.return_value = {'code': '0'}
  40. self.client.call_tool(
  41. 'MT_valid_token',
  42. 'query_order',
  43. '/mcp/tools/query_order',
  44. {'order_no': 'ABC123'},
  45. 'rq_ip_header',
  46. client_ip='203.0.113.9'
  47. )
  48. call_headers = self.mock_transport.post_json.call_args[0][2]
  49. self.assertEqual(call_headers['X-MCP-Client-IP'], '203.0.113.9')
  50. def test_call_tool_raises_on_empty_token(self):
  51. with self.assertRaises(RuntimeError) as context:
  52. self.client.call_tool('', 'query_order', '/path', {}, 'rq_1')
  53. self.assertIn('mcp token missing', str(context.exception))
  54. def test_call_tool_raises_on_none_token(self):
  55. with self.assertRaises(RuntimeError) as context:
  56. self.client.call_tool(None, 'query_order', '/path', {}, 'rq_1')
  57. self.assertIn('mcp token missing', str(context.exception))
  58. def test_call_tool_strips_leading_slash_from_route(self):
  59. self.mock_transport.post_json.return_value = {'code': '0'}
  60. self.client.call_tool('MT_token', 'tool', '/api/endpoint', {}, 'rq_1')
  61. call_url = self.mock_transport.post_json.call_args[0][0]
  62. self.assertEqual(call_url, 'http://api.example.com/api/endpoint')
  63. def test_call_tool_handles_route_without_leading_slash(self):
  64. self.mock_transport.post_json.return_value = {'code': '0'}
  65. self.client.call_tool('MT_token', 'tool', 'api/endpoint', {}, 'rq_1')
  66. call_url = self.mock_transport.post_json.call_args[0][0]
  67. self.assertEqual(call_url, 'http://api.example.com/api/endpoint')
  68. def test_call_tool_strips_trailing_slash_from_base_url(self):
  69. client_with_slash = ScopedApiClient(
  70. base_url='http://api.example.com/',
  71. transport=self.mock_transport,
  72. timeout=10
  73. )
  74. self.mock_transport.post_json.return_value = {'code': '0'}
  75. client_with_slash.call_tool('MT_token', 'tool', '/endpoint', {}, 'rq_1')
  76. call_url = self.mock_transport.post_json.call_args[0][0]
  77. # Should not have double slash
  78. self.assertEqual(call_url, 'http://api.example.com/endpoint')
  79. def test_call_tool_timeout_parameter(self):
  80. client_short_timeout = ScopedApiClient(
  81. base_url='http://api.example.com',
  82. transport=self.mock_transport,
  83. timeout=5
  84. )
  85. self.mock_transport.post_json.return_value = {'code': '0'}
  86. client_short_timeout.call_tool('MT_token', 'tool', '/path', {}, 'rq_1')
  87. # Verify timeout is passed correctly
  88. call_timeout = self.mock_transport.post_json.call_args[0][3]
  89. self.assertEqual(call_timeout, 5)
  90. def test_call_tool_with_empty_payload(self):
  91. self.mock_transport.post_json.return_value = {'code': '0'}
  92. self.client.call_tool('MT_token', 'tool', '/path', {}, 'rq_1')
  93. call_payload = self.mock_transport.post_json.call_args[0][1]
  94. self.assertEqual(call_payload, {})
  95. def test_call_tool_headers_format(self):
  96. self.mock_transport.post_json.return_value = {'code': '0'}
  97. self.client.call_tool('MT_abc123', 'query_track', '/track', {'no': '123'}, 'rq_xyz')
  98. call_headers = self.mock_transport.post_json.call_args[0][2]
  99. self.assertEqual(call_headers['Authorization'], 'Bearer MT_abc123')
  100. self.assertEqual(call_headers['X-MCP-Tool-Code'], 'query_track')
  101. self.assertEqual(call_headers['X-Request-Id'], 'rq_xyz')
  102. def test_list_enabled_tools_uses_only_explicit_token(self):
  103. expected = {
  104. 'code': 'MCP_0000',
  105. 'data': {'tool_codes': ['query_order_exact']},
  106. }
  107. self.mock_transport.post_json.return_value = expected
  108. result = self.client.list_enabled_tools(' MT_scoped ')
  109. self.assertIs(expected, result)
  110. self.mock_transport.post_json.assert_called_once_with(
  111. 'http://api.example.com/mcp/tools/listEnabledTools',
  112. {},
  113. {'Authorization': 'Bearer MT_scoped'},
  114. 15,
  115. )
  116. def test_list_enabled_tools_rejects_empty_token(self):
  117. with self.assertRaisesRegex(RuntimeError, 'mcp token missing'):
  118. self.client.list_enabled_tools('')
  119. self.mock_transport.post_json.assert_not_called()
  120. if __name__ == '__main__':
  121. unittest.main()