mcp-proxy-adapter 2.1.0__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/README.md +172 -0
- docs/README_ru.md +172 -0
- docs/architecture.md +251 -0
- docs/architecture_ru.md +343 -0
- docs/command_development.md +250 -0
- docs/command_development_ru.md +593 -0
- docs/deployment.md +251 -0
- docs/deployment_ru.md +1298 -0
- docs/examples.md +254 -0
- docs/examples_ru.md +401 -0
- docs/mcp_proxy_adapter.md +251 -0
- docs/mcp_proxy_adapter_ru.md +405 -0
- docs/quickstart.md +251 -0
- docs/quickstart_ru.md +397 -0
- docs/testing.md +255 -0
- docs/testing_ru.md +469 -0
- docs/validation_ru.md +287 -0
- examples/analyze_config.py +141 -0
- examples/basic_integration.py +161 -0
- examples/docstring_and_schema_example.py +60 -0
- examples/extension_example.py +60 -0
- examples/help_best_practices.py +67 -0
- examples/help_usage.py +64 -0
- examples/mcp_proxy_client.py +131 -0
- examples/mcp_proxy_config.json +175 -0
- examples/openapi_server.py +369 -0
- examples/project_structure_example.py +47 -0
- examples/testing_example.py +53 -0
- mcp_proxy_adapter/__init__.py +17 -0
- mcp_proxy_adapter/adapter.py +697 -0
- mcp_proxy_adapter/models.py +47 -0
- mcp_proxy_adapter/registry.py +439 -0
- mcp_proxy_adapter/schema.py +257 -0
- {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/METADATA +2 -2
- mcp_proxy_adapter-2.1.1.dist-info/RECORD +61 -0
- mcp_proxy_adapter-2.1.1.dist-info/top_level.txt +5 -0
- scripts/code_analyzer/code_analyzer.py +328 -0
- scripts/code_analyzer/register_commands.py +446 -0
- scripts/publish.py +85 -0
- tests/conftest.py +12 -0
- tests/test_adapter.py +529 -0
- tests/test_adapter_coverage.py +274 -0
- tests/test_basic_dispatcher.py +169 -0
- tests/test_command_registry.py +328 -0
- tests/test_examples.py +32 -0
- tests/test_mcp_proxy_adapter.py +568 -0
- tests/test_mcp_proxy_adapter_basic.py +262 -0
- tests/test_part1.py +348 -0
- tests/test_part2.py +524 -0
- tests/test_schema.py +358 -0
- tests/test_simple_adapter.py +251 -0
- adapters/__init__.py +0 -16
- cli/__init__.py +0 -12
- cli/__main__.py +0 -79
- cli/command_runner.py +0 -233
- generators/__init__.py +0 -14
- generators/endpoint_generator.py +0 -172
- generators/openapi_generator.py +0 -254
- generators/rest_api_generator.py +0 -207
- mcp_proxy_adapter-2.1.0.dist-info/RECORD +0 -28
- mcp_proxy_adapter-2.1.0.dist-info/top_level.txt +0 -7
- openapi_schema/__init__.py +0 -38
- openapi_schema/command_registry.py +0 -312
- openapi_schema/rest_schema.py +0 -510
- openapi_schema/rpc_generator.py +0 -307
- openapi_schema/rpc_schema.py +0 -416
- validators/__init__.py +0 -14
- validators/base_validator.py +0 -23
- {analyzers → mcp_proxy_adapter/analyzers}/__init__.py +0 -0
- {analyzers → mcp_proxy_adapter/analyzers}/docstring_analyzer.py +0 -0
- {analyzers → mcp_proxy_adapter/analyzers}/type_analyzer.py +0 -0
- {dispatchers → mcp_proxy_adapter/dispatchers}/__init__.py +0 -0
- {dispatchers → mcp_proxy_adapter/dispatchers}/base_dispatcher.py +0 -0
- {dispatchers → mcp_proxy_adapter/dispatchers}/json_rpc_dispatcher.py +0 -0
- {validators → mcp_proxy_adapter/validators}/docstring_validator.py +0 -0
- {validators → mcp_proxy_adapter/validators}/metadata_validator.py +0 -0
- {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-2.1.0.dist-info → mcp_proxy_adapter-2.1.1.dist-info}/licenses/LICENSE +0 -0
tests/test_part2.py
ADDED
@@ -0,0 +1,524 @@
|
|
1
|
+
"""
|
2
|
+
Tests for MCPProxyAdapter - Extended parameter validation and error handling.
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
import sys
|
7
|
+
import os
|
8
|
+
import pytest
|
9
|
+
import tempfile
|
10
|
+
from unittest.mock import MagicMock, patch
|
11
|
+
|
12
|
+
# Add path to source files
|
13
|
+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
14
|
+
if project_root not in sys.path:
|
15
|
+
sys.path.insert(0, project_root)
|
16
|
+
|
17
|
+
from fastapi import FastAPI, APIRouter
|
18
|
+
from fastapi.testclient import TestClient
|
19
|
+
|
20
|
+
# Import directly from src
|
21
|
+
from mcp_proxy_adapter.adapter import MCPProxyAdapter, configure_logger, SchemaOptimizer
|
22
|
+
from mcp_proxy_adapter.models import JsonRpcRequest, JsonRpcResponse, MCPProxyConfig, MCPProxyTool
|
23
|
+
|
24
|
+
# Import common test components
|
25
|
+
from tests.test_mcp_proxy_adapter import (
|
26
|
+
MockDispatcher,
|
27
|
+
MockRegistry,
|
28
|
+
MockOpenApiGenerator,
|
29
|
+
success_command,
|
30
|
+
error_command,
|
31
|
+
param_command,
|
32
|
+
complex_param_command,
|
33
|
+
type_error_command
|
34
|
+
)
|
35
|
+
|
36
|
+
# Переопределяем MockDispatcher для этого файла, чтобы добавить complex_param и type_error
|
37
|
+
class PatchedMockDispatcher(MockDispatcher):
|
38
|
+
def __init__(self):
|
39
|
+
super().__init__()
|
40
|
+
self.commands["complex_param"] = complex_param_command
|
41
|
+
self.commands["type_error"] = type_error_command
|
42
|
+
self.commands_info["complex_param"] = {
|
43
|
+
"description": "Command with complex parameters",
|
44
|
+
"params": {
|
45
|
+
"array_param": {
|
46
|
+
"type": "array",
|
47
|
+
"description": "Array of values",
|
48
|
+
"required": True
|
49
|
+
},
|
50
|
+
"object_param": {
|
51
|
+
"type": "object",
|
52
|
+
"description": "Object",
|
53
|
+
"required": True
|
54
|
+
},
|
55
|
+
"bool_param": {
|
56
|
+
"type": "boolean",
|
57
|
+
"description": "Boolean value",
|
58
|
+
"required": False,
|
59
|
+
"default": True
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
self.commands_info["type_error"] = {
|
64
|
+
"description": "Command that will raise TypeError",
|
65
|
+
"params": {
|
66
|
+
"param": {
|
67
|
+
"type": "integer",
|
68
|
+
"description": "Integer parameter",
|
69
|
+
"required": True
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
class PatchedMockRegistry(MockRegistry):
|
75
|
+
def __init__(self):
|
76
|
+
super().__init__()
|
77
|
+
self.dispatcher = PatchedMockDispatcher()
|
78
|
+
|
79
|
+
@pytest.fixture
|
80
|
+
def registry():
|
81
|
+
return PatchedMockRegistry()
|
82
|
+
|
83
|
+
@pytest.fixture
|
84
|
+
def adapter(registry):
|
85
|
+
"""Returns a configured adapter for tests."""
|
86
|
+
return MCPProxyAdapter(registry)
|
87
|
+
|
88
|
+
@pytest.fixture
|
89
|
+
def test_app(adapter):
|
90
|
+
"""Creates a FastAPI test application with a configured adapter."""
|
91
|
+
app = FastAPI()
|
92
|
+
adapter.register_endpoints(app)
|
93
|
+
return TestClient(app)
|
94
|
+
|
95
|
+
@pytest.fixture
|
96
|
+
def custom_endpoint_adapter(registry):
|
97
|
+
"""Returns an adapter with a custom endpoint."""
|
98
|
+
return MCPProxyAdapter(registry, cmd_endpoint="/api/execute")
|
99
|
+
|
100
|
+
@pytest.fixture
|
101
|
+
def custom_endpoint_app(custom_endpoint_adapter):
|
102
|
+
"""Creates a test application with an adapter having a custom endpoint."""
|
103
|
+
app = FastAPI()
|
104
|
+
custom_endpoint_adapter.register_endpoints(app)
|
105
|
+
return TestClient(app)
|
106
|
+
|
107
|
+
@pytest.fixture
|
108
|
+
def no_schema_adapter(registry):
|
109
|
+
"""Returns an adapter without including OpenAPI schema."""
|
110
|
+
return MCPProxyAdapter(registry, include_schema=False)
|
111
|
+
|
112
|
+
@pytest.fixture
|
113
|
+
def adapter_with_openapi(registry):
|
114
|
+
"""Returns an adapter with OpenAPI support for tests."""
|
115
|
+
with patch('mcp_proxy_adapter.adapter.OpenApiGenerator', MockOpenApiGenerator):
|
116
|
+
return MCPProxyAdapter(registry)
|
117
|
+
|
118
|
+
@pytest.fixture
|
119
|
+
def custom_prefix_adapter(registry):
|
120
|
+
"""Returns an adapter with a custom tool prefix."""
|
121
|
+
return MCPProxyAdapter(registry, tool_name_prefix="custom_")
|
122
|
+
|
123
|
+
@pytest.fixture
|
124
|
+
def custom_logger():
|
125
|
+
"""Creates a custom logger for tests."""
|
126
|
+
logger = logging.getLogger("test_logger")
|
127
|
+
logger.setLevel(logging.DEBUG)
|
128
|
+
|
129
|
+
# Clear handlers if they were added in previous tests
|
130
|
+
if logger.handlers:
|
131
|
+
logger.handlers = []
|
132
|
+
|
133
|
+
# Add handler that will write messages to list
|
134
|
+
log_records = []
|
135
|
+
|
136
|
+
class ListHandler(logging.Handler):
|
137
|
+
def emit(self, record):
|
138
|
+
log_records.append(self.format(record))
|
139
|
+
|
140
|
+
handler = ListHandler()
|
141
|
+
formatter = logging.Formatter('%(levelname)s:%(name)s:%(message)s')
|
142
|
+
handler.setFormatter(formatter)
|
143
|
+
logger.addHandler(handler)
|
144
|
+
|
145
|
+
return logger, log_records
|
146
|
+
|
147
|
+
def test_error_command_execution(test_app):
|
148
|
+
"""Test error handling when executing a command."""
|
149
|
+
response = test_app.post("/cmd", json={
|
150
|
+
"jsonrpc": "2.0",
|
151
|
+
"method": "error",
|
152
|
+
"id": 1
|
153
|
+
})
|
154
|
+
assert response.status_code == 200 # JSON-RPC always returns 200
|
155
|
+
data = response.json()
|
156
|
+
assert "error" in data
|
157
|
+
assert "Тестовая ошибка" in data["error"]["message"] or "Test error" in data["error"]["message"]
|
158
|
+
|
159
|
+
def test_unknown_command(test_app):
|
160
|
+
"""Test handling of unknown command."""
|
161
|
+
response = test_app.post("/cmd", json={
|
162
|
+
"jsonrpc": "2.0",
|
163
|
+
"method": "unknown_command",
|
164
|
+
"id": 1
|
165
|
+
})
|
166
|
+
assert response.status_code == 200 # JSON-RPC always returns 200
|
167
|
+
data = response.json()
|
168
|
+
assert "error" in data
|
169
|
+
assert "Unknown command" in data["error"]["message"]
|
170
|
+
|
171
|
+
def test_missing_required_parameter(test_app):
|
172
|
+
"""Test handling of missing required parameter."""
|
173
|
+
response = test_app.post("/cmd", json={
|
174
|
+
"jsonrpc": "2.0",
|
175
|
+
"method": "param",
|
176
|
+
"params": {}, # Missing required parameter required_param
|
177
|
+
"id": 1
|
178
|
+
})
|
179
|
+
assert response.status_code == 200 # JSON-RPC always returns 200
|
180
|
+
data = response.json()
|
181
|
+
assert "error" in data
|
182
|
+
assert "required_param" in data["error"]["message"].lower()
|
183
|
+
|
184
|
+
def test_custom_endpoint(custom_endpoint_app):
|
185
|
+
"""Test adapter working with custom endpoint."""
|
186
|
+
# Проверяем, что стандартный эндпоинт доступен
|
187
|
+
response = custom_endpoint_app.post("/cmd", json={
|
188
|
+
"jsonrpc": "2.0",
|
189
|
+
"method": "success",
|
190
|
+
"params": {"value": 5},
|
191
|
+
"id": 1
|
192
|
+
})
|
193
|
+
assert response.status_code == 200
|
194
|
+
data = response.json()
|
195
|
+
assert data["result"] == {"result": 10}
|
196
|
+
|
197
|
+
# Проверяем, что кастомный эндпоинт работает
|
198
|
+
response = custom_endpoint_app.post("/api/execute", json={
|
199
|
+
"jsonrpc": "2.0",
|
200
|
+
"method": "success",
|
201
|
+
"params": {"value": 5},
|
202
|
+
"id": 1
|
203
|
+
})
|
204
|
+
assert response.status_code == 200
|
205
|
+
data = response.json()
|
206
|
+
assert data["result"] == {"result": 10}
|
207
|
+
|
208
|
+
# Test parameter type validation
|
209
|
+
def test_string_parameter_validation(test_app):
|
210
|
+
"""Test string parameter validation."""
|
211
|
+
response = test_app.post("/cmd", json={
|
212
|
+
"jsonrpc": "2.0",
|
213
|
+
"method": "param",
|
214
|
+
"params": {
|
215
|
+
"required_param": 123, # Must be a string
|
216
|
+
"optional_param": 0
|
217
|
+
},
|
218
|
+
"id": 1
|
219
|
+
})
|
220
|
+
assert response.status_code == 200
|
221
|
+
data = response.json()
|
222
|
+
assert "error" in data
|
223
|
+
assert "required_param" in data["error"]["message"]
|
224
|
+
assert "must be a string" in data["error"]["message"]
|
225
|
+
|
226
|
+
def test_integer_parameter_validation(test_app):
|
227
|
+
"""Test integer parameter validation."""
|
228
|
+
response = test_app.post("/cmd", json={
|
229
|
+
"jsonrpc": "2.0",
|
230
|
+
"method": "param",
|
231
|
+
"params": {
|
232
|
+
"required_param": "test",
|
233
|
+
"optional_param": "not_integer" # Must be an integer
|
234
|
+
},
|
235
|
+
"id": 1
|
236
|
+
})
|
237
|
+
assert response.status_code == 200
|
238
|
+
data = response.json()
|
239
|
+
assert "error" in data
|
240
|
+
assert "optional_param" in data["error"]["message"]
|
241
|
+
assert "must be an integer" in data["error"]["message"]
|
242
|
+
|
243
|
+
def test_array_parameter_validation(test_app):
|
244
|
+
"""Test array parameter validation."""
|
245
|
+
response = test_app.post("/cmd", json={
|
246
|
+
"jsonrpc": "2.0",
|
247
|
+
"method": "complex_param",
|
248
|
+
"params": {
|
249
|
+
"array_param": "not_array", # Must be an array
|
250
|
+
"object_param": {}
|
251
|
+
},
|
252
|
+
"id": 1
|
253
|
+
})
|
254
|
+
assert response.status_code == 200
|
255
|
+
data = response.json()
|
256
|
+
assert "error" in data
|
257
|
+
assert "array_param" in data["error"]["message"]
|
258
|
+
assert "must be an array" in data["error"]["message"]
|
259
|
+
|
260
|
+
def test_object_parameter_validation(test_app):
|
261
|
+
"""Test object parameter validation."""
|
262
|
+
response = test_app.post("/cmd", json={
|
263
|
+
"jsonrpc": "2.0",
|
264
|
+
"method": "complex_param",
|
265
|
+
"params": {
|
266
|
+
"array_param": [1, 2, 3],
|
267
|
+
"object_param": "not_object" # Must be an object
|
268
|
+
},
|
269
|
+
"id": 1
|
270
|
+
})
|
271
|
+
assert response.status_code == 200
|
272
|
+
data = response.json()
|
273
|
+
assert "error" in data
|
274
|
+
assert "object_param" in data["error"]["message"]
|
275
|
+
assert "must be an object" in data["error"]["message"]
|
276
|
+
|
277
|
+
def test_boolean_parameter_validation(test_app):
|
278
|
+
"""Test boolean parameter validation."""
|
279
|
+
response = test_app.post("/cmd", json={
|
280
|
+
"jsonrpc": "2.0",
|
281
|
+
"method": "complex_param",
|
282
|
+
"params": {
|
283
|
+
"array_param": [1, 2, 3],
|
284
|
+
"object_param": {"key": "value"},
|
285
|
+
"bool_param": "not_boolean" # Must be a boolean value
|
286
|
+
},
|
287
|
+
"id": 1
|
288
|
+
})
|
289
|
+
assert response.status_code == 200
|
290
|
+
data = response.json()
|
291
|
+
assert "error" in data
|
292
|
+
assert "bool_param" in data["error"]["message"]
|
293
|
+
assert "must be a boolean" in data["error"]["message"]
|
294
|
+
|
295
|
+
# Test error handling
|
296
|
+
def test_type_error_command(test_app):
|
297
|
+
"""Test TypeError handling when executing a command."""
|
298
|
+
response = test_app.post("/cmd", json={
|
299
|
+
"jsonrpc": "2.0",
|
300
|
+
"method": "type_error",
|
301
|
+
"params": {
|
302
|
+
"param": "not_int" # Will cause TypeError inside function
|
303
|
+
},
|
304
|
+
"id": 1
|
305
|
+
})
|
306
|
+
assert response.status_code == 200
|
307
|
+
data = response.json()
|
308
|
+
assert "error" in data
|
309
|
+
assert data["error"]["code"] in [400, -32602, 404] # Acceptable codes
|
310
|
+
assert "param" in data["error"]["message"]
|
311
|
+
assert "must be an integer" in data["error"]["message"]
|
312
|
+
|
313
|
+
def test_unexpected_error_handling(test_app):
|
314
|
+
"""Test handling of unexpected error."""
|
315
|
+
with patch.object(MockDispatcher, 'execute', side_effect=Exception("Unexpected error")):
|
316
|
+
response = test_app.post("/cmd", json={
|
317
|
+
"jsonrpc": "2.0",
|
318
|
+
"method": "success",
|
319
|
+
"params": {"value": 5},
|
320
|
+
"id": 1
|
321
|
+
})
|
322
|
+
assert response.status_code == 200
|
323
|
+
data = response.json()
|
324
|
+
assert "error" in data
|
325
|
+
assert data["error"]["code"] in [500, -32603]
|
326
|
+
assert "Unexpected error" in data["error"]["message"]
|
327
|
+
|
328
|
+
# Test configuration and schema generation
|
329
|
+
def test_no_schema_endpoint(no_schema_adapter):
|
330
|
+
"""Test absence of schema endpoint if include_schema=False."""
|
331
|
+
# Check that adapter is configured correctly
|
332
|
+
assert not no_schema_adapter.include_schema
|
333
|
+
|
334
|
+
# Check that there is no OpenAPI schema endpoint in routes
|
335
|
+
routes = [route.path for route in no_schema_adapter.router.routes]
|
336
|
+
assert "/openapi.json" not in routes
|
337
|
+
|
338
|
+
def test_schema_endpoint(test_app):
|
339
|
+
"""Test presence of schema endpoint if include_schema=True."""
|
340
|
+
response = test_app.get("/openapi.json")
|
341
|
+
assert response.status_code == 200
|
342
|
+
data = response.json()
|
343
|
+
assert "openapi" in data
|
344
|
+
|
345
|
+
def test_openapi_generator_integration(adapter_with_openapi):
|
346
|
+
"""Test OpenApiGenerator integration."""
|
347
|
+
schema = adapter_with_openapi._generate_basic_schema()
|
348
|
+
assert "openapi" in schema
|
349
|
+
assert "info" in schema
|
350
|
+
assert "paths" in schema
|
351
|
+
|
352
|
+
def test_schema_without_openapi_generator(adapter):
|
353
|
+
"""Test schema generation without OpenApiGenerator."""
|
354
|
+
schema = adapter._generate_basic_schema()
|
355
|
+
assert "openapi" in schema
|
356
|
+
assert "info" in schema
|
357
|
+
assert "paths" in schema
|
358
|
+
|
359
|
+
def test_config_generation(adapter):
|
360
|
+
"""Test configuration generation for MCPProxy."""
|
361
|
+
config = adapter.generate_mcp_proxy_config()
|
362
|
+
# Check configuration structure (устойчивая проверка)
|
363
|
+
assert type(config).__name__ == "MCPProxyConfig"
|
364
|
+
assert hasattr(config, "tools") and isinstance(config.tools, list)
|
365
|
+
assert hasattr(config, "routes") and isinstance(config.routes, list)
|
366
|
+
assert hasattr(config, "version") and config.version == "1.0"
|
367
|
+
# Check presence of tools
|
368
|
+
assert len(config.tools) > 0
|
369
|
+
# Check presence of routes
|
370
|
+
assert len(config.routes) > 0
|
371
|
+
# Check tools and routes correspondence
|
372
|
+
tool_names = [tool.name for tool in config.tools]
|
373
|
+
route_tools = [route["tool_name"] for route in config.routes]
|
374
|
+
assert all(name in tool_names for name in route_tools)
|
375
|
+
|
376
|
+
def test_config_with_custom_prefix(custom_prefix_adapter):
|
377
|
+
"""Test configuration generation with custom prefix."""
|
378
|
+
config = custom_prefix_adapter.generate_mcp_proxy_config()
|
379
|
+
tool_names = [tool.name for tool in config.tools]
|
380
|
+
for name in tool_names:
|
381
|
+
assert name.startswith("custom_")
|
382
|
+
|
383
|
+
def test_save_config_to_file():
|
384
|
+
"""Test saving configuration to file."""
|
385
|
+
registry = MockRegistry()
|
386
|
+
adapter = MCPProxyAdapter(registry)
|
387
|
+
|
388
|
+
with tempfile.NamedTemporaryFile(suffix='.json') as temp_file:
|
389
|
+
adapter.save_config_to_file(temp_file.name)
|
390
|
+
|
391
|
+
# Check that file is not empty
|
392
|
+
assert os.path.getsize(temp_file.name) > 0
|
393
|
+
|
394
|
+
# Load and check content
|
395
|
+
with open(temp_file.name, 'r') as f:
|
396
|
+
config_data = json.load(f)
|
397
|
+
assert "routes" in config_data
|
398
|
+
assert "tools" in config_data
|
399
|
+
|
400
|
+
def test_from_registry_classmethod():
|
401
|
+
"""Test creating adapter through from_registry class method."""
|
402
|
+
registry = MockRegistry()
|
403
|
+
adapter = MCPProxyAdapter.from_registry(registry, cmd_endpoint="/custom", tool_name_prefix="test_")
|
404
|
+
|
405
|
+
assert adapter.cmd_endpoint == "/custom"
|
406
|
+
assert adapter.tool_name_prefix == "test_"
|
407
|
+
|
408
|
+
def test_logger_configuration():
|
409
|
+
"""Test logger configuration."""
|
410
|
+
# Test with creating new logger
|
411
|
+
logger1 = configure_logger()
|
412
|
+
assert logger1.name == "mcp_proxy_adapter"
|
413
|
+
|
414
|
+
# Test with parent logger
|
415
|
+
parent_logger = logging.getLogger("parent")
|
416
|
+
logger2 = configure_logger(parent_logger)
|
417
|
+
assert logger2.name == "parent.mcp_proxy_adapter"
|
418
|
+
|
419
|
+
def test_custom_logger_integration(custom_logger):
|
420
|
+
"""Test integration with custom logger."""
|
421
|
+
logger, log_records = custom_logger
|
422
|
+
|
423
|
+
# Configure adapter with custom logger
|
424
|
+
with patch('mcp_proxy_adapter.adapter.logger', logger):
|
425
|
+
registry = MockRegistry()
|
426
|
+
adapter = MCPProxyAdapter(registry)
|
427
|
+
|
428
|
+
# Create test application
|
429
|
+
app = FastAPI()
|
430
|
+
adapter.register_endpoints(app)
|
431
|
+
client = TestClient(app)
|
432
|
+
|
433
|
+
# Call unknown command to trigger logging
|
434
|
+
client.post("/cmd", json={
|
435
|
+
"jsonrpc": "2.0",
|
436
|
+
"method": "unknown_command",
|
437
|
+
"id": 1
|
438
|
+
})
|
439
|
+
|
440
|
+
# Check that message was logged
|
441
|
+
assert any("unknown_command" in record for record in log_records)
|
442
|
+
|
443
|
+
# Test edge cases
|
444
|
+
def test_command_with_empty_info(test_app):
|
445
|
+
"""Test command with empty parameter information."""
|
446
|
+
# Patch get_command_info to return empty dictionary
|
447
|
+
with patch.object(MockDispatcher, 'get_command_info', return_value={}):
|
448
|
+
response = test_app.post("/cmd", json={
|
449
|
+
"jsonrpc": "2.0",
|
450
|
+
"method": "success",
|
451
|
+
"params": {"value": 5},
|
452
|
+
"id": 1
|
453
|
+
})
|
454
|
+
assert response.status_code == 200
|
455
|
+
data = response.json()
|
456
|
+
assert "result" in data
|
457
|
+
|
458
|
+
def test_command_with_none_info(test_app):
|
459
|
+
"""Test command with missing information."""
|
460
|
+
# Patch get_command_info to return None
|
461
|
+
with patch.object(MockDispatcher, 'get_command_info', return_value=None):
|
462
|
+
response = test_app.post("/cmd", json={
|
463
|
+
"jsonrpc": "2.0",
|
464
|
+
"method": "success",
|
465
|
+
"params": {"value": 5},
|
466
|
+
"id": 1
|
467
|
+
})
|
468
|
+
assert response.status_code == 200
|
469
|
+
data = response.json()
|
470
|
+
assert "result" in data
|
471
|
+
|
472
|
+
def test_command_info_without_params(test_app):
|
473
|
+
"""Test command with information, not containing parameters."""
|
474
|
+
# Patch get_command_info to return dictionary without params
|
475
|
+
with patch.object(MockDispatcher, 'get_command_info', return_value={"description": "Test"}):
|
476
|
+
response = test_app.post("/cmd", json={
|
477
|
+
"jsonrpc": "2.0",
|
478
|
+
"method": "success",
|
479
|
+
"params": {"value": 5},
|
480
|
+
"id": 1
|
481
|
+
})
|
482
|
+
assert response.status_code == 200
|
483
|
+
data = response.json()
|
484
|
+
assert "result" in data
|
485
|
+
|
486
|
+
def test_command_params_without_type(test_app):
|
487
|
+
"""Test command parameters without type indication."""
|
488
|
+
# Create command information with parameter without type
|
489
|
+
command_info = {
|
490
|
+
"description": "Test",
|
491
|
+
"params": {
|
492
|
+
"value": {
|
493
|
+
"description": "Value without type",
|
494
|
+
"required": True
|
495
|
+
}
|
496
|
+
}
|
497
|
+
}
|
498
|
+
|
499
|
+
# Patch get_command_info
|
500
|
+
with patch.object(MockDispatcher, 'get_command_info', return_value=command_info):
|
501
|
+
response = test_app.post("/cmd", json={
|
502
|
+
"jsonrpc": "2.0",
|
503
|
+
"method": "success",
|
504
|
+
"params": {"value": "test"},
|
505
|
+
"id": 1
|
506
|
+
})
|
507
|
+
assert response.status_code == 200
|
508
|
+
data = response.json()
|
509
|
+
assert "result" in data
|
510
|
+
|
511
|
+
def test_api_commands_endpoint(test_app):
|
512
|
+
"""Test endpoint for getting information about all commands."""
|
513
|
+
response = test_app.get("/api/commands")
|
514
|
+
assert response.status_code == 200
|
515
|
+
# In real adapter commands may be in different structure,
|
516
|
+
# let's check more general case - presence of answer in JSON format
|
517
|
+
data = response.json()
|
518
|
+
assert isinstance(data, dict)
|
519
|
+
# Check that returned data about commands (without specific structure)
|
520
|
+
assert len(data) > 0
|
521
|
+
assert "success" in str(data) # Command name must be present somewhere in answer
|
522
|
+
|
523
|
+
if __name__ == "__main__":
|
524
|
+
pytest.main(["-v", __file__])
|