|
|
@@ -12,6 +12,7 @@ import json
|
|
|
import os
|
|
|
import tempfile
|
|
|
import unittest
|
|
|
+from types import SimpleNamespace
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
from app import GatewayApp, main
|
|
|
@@ -101,6 +102,43 @@ class DummyAuthClient:
|
|
|
pass
|
|
|
|
|
|
|
|
|
+class GatewayAppBoundaryTest(unittest.TestCase):
|
|
|
+ def test_registered_tool_names_returns_local_registry(self):
|
|
|
+ app = GatewayApp(api_client=DummyApiClient())
|
|
|
+
|
|
|
+ self.assertEqual(tuple(app._tools.keys()), app.registered_tool_names())
|
|
|
+
|
|
|
+ def test_enabled_tool_names_rejects_non_dict_response(self):
|
|
|
+ with self.assertRaisesRegex(RuntimeError, 'invalid enabled tool response'):
|
|
|
+ GatewayApp()._enabled_tool_names(None)
|
|
|
+
|
|
|
+ def test_enabled_tool_names_rejects_missing_tool_code_list(self):
|
|
|
+ response = {'code': 'MCP_0000', 'data': {}}
|
|
|
+
|
|
|
+ with self.assertRaisesRegex(RuntimeError, 'invalid enabled tool response'):
|
|
|
+ GatewayApp()._enabled_tool_names(response)
|
|
|
+
|
|
|
+ def test_load_enabled_tools_requires_capable_client(self):
|
|
|
+ for api_client in (None, object()):
|
|
|
+ with self.subTest(api_client=api_client):
|
|
|
+ with self.assertRaisesRegex(RuntimeError, 'client unavailable'):
|
|
|
+ GatewayApp(api_client=api_client)._load_enabled_tool_names()
|
|
|
+
|
|
|
+ def test_build_request_id_preserves_provided_value(self):
|
|
|
+ self.assertEqual(
|
|
|
+ 'rq_given',
|
|
|
+ GatewayApp().build_request_id(' rq_given '),
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_ensure_session_requires_auth_client_for_expiring_token(self):
|
|
|
+ token_store = MagicMock()
|
|
|
+ token_store.get.return_value = {'token': 'MT_test'}
|
|
|
+ token_store.is_expiring.return_value = True
|
|
|
+
|
|
|
+ with self.assertRaisesRegex(RuntimeError, 'auth client missing'):
|
|
|
+ GatewayApp(token_store=token_store).ensure_session()
|
|
|
+
|
|
|
+
|
|
|
def _make_app_with_token():
|
|
|
"""创建有有效 token 的 GatewayApp,附带一个自定义 fake_tool 工具。"""
|
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
|
@@ -145,6 +183,125 @@ class RunCliCallElseBranchTest(unittest.TestCase):
|
|
|
payload = json.loads(stdout.getvalue())
|
|
|
self.assertEqual('MCP_0000', payload['code'])
|
|
|
|
|
|
+
|
|
|
+class RunCliBranchCoverageTest(unittest.TestCase):
|
|
|
+ def _run_with_mocked_call(self, argv):
|
|
|
+ app = GatewayApp(api_client=DummyApiClient())
|
|
|
+ stdout = io.StringIO()
|
|
|
+ with patch.object(
|
|
|
+ app,
|
|
|
+ 'call_tool',
|
|
|
+ return_value={'code': 'MCP_0000'},
|
|
|
+ ) as call_tool:
|
|
|
+ result = app.run_cli(argv, stdout=stdout)
|
|
|
+ return result, call_tool
|
|
|
+
|
|
|
+ def test_query_order_requires_keyword(self):
|
|
|
+ with self.assertRaisesRegex(ValueError, 'keyword is required'):
|
|
|
+ GatewayApp().run_cli([
|
|
|
+ 'call', '--tool', 'query_order',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+
|
|
|
+ def test_query_track_forwards_each_supported_identifier(self):
|
|
|
+ cases = (
|
|
|
+ (['--order-id', '5'], {'order_id': 5}),
|
|
|
+ (['--order-number', 'ORDER-1'], {'order_number': 'ORDER-1'}),
|
|
|
+ (['--tracking-number', 'TRACK-1'], {'tracking_number': 'TRACK-1'}),
|
|
|
+ )
|
|
|
+ for cli_args, expected in cases:
|
|
|
+ with self.subTest(cli_args=cli_args):
|
|
|
+ result, call_tool = self._run_with_mocked_call([
|
|
|
+ 'call', '--tool', 'query_track', *cli_args,
|
|
|
+ ])
|
|
|
+ self.assertEqual(0, result)
|
|
|
+ payload = call_tool.call_args.args[1]
|
|
|
+ for key, value in expected.items():
|
|
|
+ self.assertEqual(value, payload[key])
|
|
|
+
|
|
|
+ def test_query_track_requires_one_identifier(self):
|
|
|
+ with self.assertRaisesRegex(ValueError, 'order-id'):
|
|
|
+ GatewayApp().run_cli([
|
|
|
+ 'call', '--tool', 'query_track',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+
|
|
|
+ def test_query_order_exact_forwards_scalar_ids(self):
|
|
|
+ result, call_tool = self._run_with_mocked_call([
|
|
|
+ 'call', '--tool', 'query_order_exact',
|
|
|
+ '--order-number', 'ORDER-1',
|
|
|
+ '--sales-id', '7',
|
|
|
+ '--department-id', '8',
|
|
|
+ ])
|
|
|
+
|
|
|
+ self.assertEqual(0, result)
|
|
|
+ payload = call_tool.call_args.args[1]
|
|
|
+ self.assertEqual(7, payload['sales_id'])
|
|
|
+ self.assertEqual(8, payload['department_id'])
|
|
|
+
|
|
|
+ def test_filter_options_requires_filter_type(self):
|
|
|
+ with self.assertRaisesRegex(ValueError, 'filter-type is required'):
|
|
|
+ GatewayApp().run_cli([
|
|
|
+ 'call', '--tool', 'list_order_filter_options',
|
|
|
+ ], stdout=io.StringIO())
|
|
|
+
|
|
|
+ def test_serve_public_builds_scoped_dependencies(self):
|
|
|
+ config = SimpleNamespace(
|
|
|
+ redis_host='redis.test',
|
|
|
+ redis_port=6380,
|
|
|
+ redis_db=2,
|
|
|
+ redis_password='secret',
|
|
|
+ timeout_seconds=9,
|
|
|
+ redis_prefix='gateway:',
|
|
|
+ gateway_session_ttl_seconds=600,
|
|
|
+ tools_base_url='https://tools.test',
|
|
|
+ rate_limit_enabled=True,
|
|
|
+ rate_limit_max_requests=12,
|
|
|
+ rate_limit_window_seconds=34,
|
|
|
+ )
|
|
|
+ with patch('app.GatewayConfig.from_env', return_value=config), \
|
|
|
+ patch('app.RedisSocketClient') as redis_cls, \
|
|
|
+ patch('app.GatewaySessionStore') as store_cls, \
|
|
|
+ patch('app.ScopedApiClient') as api_cls, \
|
|
|
+ patch('app.PublicGatewayApp') as public_app_cls, \
|
|
|
+ patch('app.serve_public', return_value=17) as serve:
|
|
|
+ result = GatewayApp().run_cli([
|
|
|
+ 'serve-public', '--host', '127.0.0.1', '--port', '9000',
|
|
|
+ ])
|
|
|
+
|
|
|
+ self.assertEqual(17, result)
|
|
|
+ redis_cls.assert_called_once_with(
|
|
|
+ host='redis.test',
|
|
|
+ port=6380,
|
|
|
+ db=2,
|
|
|
+ password='secret',
|
|
|
+ timeout=9,
|
|
|
+ )
|
|
|
+ store_cls.assert_called_once_with(
|
|
|
+ redis_cls.return_value,
|
|
|
+ prefix='gateway:',
|
|
|
+ ttl_seconds=600,
|
|
|
+ )
|
|
|
+ api_cls.assert_called_once_with('https://tools.test', timeout=9)
|
|
|
+ public_app_cls.assert_called_once_with(
|
|
|
+ session_store=store_cls.return_value,
|
|
|
+ api_client=api_cls.return_value,
|
|
|
+ )
|
|
|
+ serve.assert_called_once_with(
|
|
|
+ public_app_cls.return_value,
|
|
|
+ host='127.0.0.1',
|
|
|
+ port=9000,
|
|
|
+ enable_rate_limit=True,
|
|
|
+ rate_limit_max_requests=12,
|
|
|
+ rate_limit_window_seconds=34,
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_unsupported_command_raises_runtime_error(self):
|
|
|
+ with patch(
|
|
|
+ 'app.argparse.ArgumentParser.parse_args',
|
|
|
+ return_value=SimpleNamespace(command='unknown'),
|
|
|
+ ):
|
|
|
+ with self.assertRaisesRegex(RuntimeError, 'unsupported command'):
|
|
|
+ GatewayApp().run_cli([], stdout=io.StringIO())
|
|
|
+
|
|
|
def test_call_else_branch_without_keyword_still_calls_tool(self):
|
|
|
"""
|
|
|
--tool fake_tool(非 query_order / query_track)且 --keyword 为空 →
|