mcp-proxy-adapter 3.0.0__py3-none-any.whl → 3.0.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.
- examples/basic_example/README.md +123 -9
- examples/basic_example/config.json +4 -0
- examples/basic_example/docs/EN/README.md +46 -5
- examples/basic_example/docs/RU/README.md +46 -5
- examples/basic_example/server.py +127 -21
- examples/complete_example/commands/system_command.py +1 -0
- examples/complete_example/server.py +65 -11
- examples/minimal_example/README.md +20 -6
- examples/minimal_example/config.json +7 -14
- examples/minimal_example/main.py +109 -40
- examples/minimal_example/simple_server.py +53 -14
- examples/minimal_example/tests/conftest.py +1 -1
- examples/minimal_example/tests/test_integration.py +8 -10
- examples/simple_server.py +12 -21
- examples/test_server.py +22 -14
- examples/tool_description_example.py +82 -0
- mcp_proxy_adapter/api/__init__.py +0 -0
- mcp_proxy_adapter/api/app.py +391 -0
- mcp_proxy_adapter/api/handlers.py +229 -0
- mcp_proxy_adapter/api/middleware/__init__.py +49 -0
- mcp_proxy_adapter/api/middleware/auth.py +146 -0
- mcp_proxy_adapter/api/middleware/base.py +79 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +198 -0
- mcp_proxy_adapter/api/middleware/logging.py +96 -0
- mcp_proxy_adapter/api/middleware/performance.py +83 -0
- mcp_proxy_adapter/api/middleware/rate_limit.py +152 -0
- mcp_proxy_adapter/api/schemas.py +305 -0
- mcp_proxy_adapter/api/tool_integration.py +223 -0
- mcp_proxy_adapter/api/tools.py +198 -0
- mcp_proxy_adapter/commands/__init__.py +19 -0
- mcp_proxy_adapter/commands/base.py +301 -0
- mcp_proxy_adapter/commands/command_registry.py +231 -0
- mcp_proxy_adapter/commands/config_command.py +113 -0
- mcp_proxy_adapter/commands/health_command.py +136 -0
- mcp_proxy_adapter/commands/help_command.py +193 -0
- mcp_proxy_adapter/commands/result.py +215 -0
- mcp_proxy_adapter/config.py +9 -0
- mcp_proxy_adapter/core/__init__.py +0 -0
- mcp_proxy_adapter/core/errors.py +173 -0
- mcp_proxy_adapter/core/logging.py +205 -0
- mcp_proxy_adapter/core/utils.py +138 -0
- mcp_proxy_adapter/py.typed +0 -0
- mcp_proxy_adapter/schemas/base_schema.json +114 -0
- mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +3 -0
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +115 -0
- mcp_proxy_adapter/tests/api/test_middleware.py +336 -0
- mcp_proxy_adapter/tests/commands/__init__.py +3 -0
- mcp_proxy_adapter/tests/commands/test_config_command.py +211 -0
- mcp_proxy_adapter/tests/commands/test_echo_command.py +127 -0
- mcp_proxy_adapter/tests/commands/test_help_command.py +133 -0
- mcp_proxy_adapter/tests/conftest.py +131 -0
- mcp_proxy_adapter/tests/functional/__init__.py +3 -0
- mcp_proxy_adapter/tests/functional/test_api.py +235 -0
- mcp_proxy_adapter/tests/integration/__init__.py +3 -0
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +130 -0
- mcp_proxy_adapter/tests/integration/test_integration.py +255 -0
- mcp_proxy_adapter/tests/performance/__init__.py +3 -0
- mcp_proxy_adapter/tests/performance/test_performance.py +189 -0
- mcp_proxy_adapter/tests/stubs/__init__.py +10 -0
- mcp_proxy_adapter/tests/stubs/echo_command.py +104 -0
- mcp_proxy_adapter/tests/test_api_endpoints.py +271 -0
- mcp_proxy_adapter/tests/test_api_handlers.py +289 -0
- mcp_proxy_adapter/tests/test_base_command.py +123 -0
- mcp_proxy_adapter/tests/test_batch_requests.py +117 -0
- mcp_proxy_adapter/tests/test_command_registry.py +245 -0
- mcp_proxy_adapter/tests/test_config.py +127 -0
- mcp_proxy_adapter/tests/test_utils.py +65 -0
- mcp_proxy_adapter/tests/unit/__init__.py +3 -0
- mcp_proxy_adapter/tests/unit/test_base_command.py +130 -0
- mcp_proxy_adapter/tests/unit/test_config.py +217 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/METADATA +1 -1
- mcp_proxy_adapter-3.0.1.dist-info/RECORD +109 -0
- examples/basic_example/config.yaml +0 -20
- examples/basic_example/main.py +0 -50
- examples/complete_example/main.py +0 -67
- examples/minimal_example/config.yaml +0 -26
- mcp_proxy_adapter/framework.py +0 -109
- mcp_proxy_adapter-3.0.0.dist-info/RECORD +0 -58
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-3.0.0.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,336 @@
|
|
1
|
+
"""
|
2
|
+
Tests for middleware components.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
from unittest.mock import patch, MagicMock, AsyncMock
|
7
|
+
import json
|
8
|
+
import time
|
9
|
+
|
10
|
+
from fastapi import FastAPI, Request, Response
|
11
|
+
from fastapi.testclient import TestClient
|
12
|
+
from starlette.applications import Starlette
|
13
|
+
from starlette.responses import JSONResponse
|
14
|
+
from starlette.routing import Route
|
15
|
+
|
16
|
+
from mcp_proxy_adapter.api.middleware.base import BaseMiddleware
|
17
|
+
from mcp_proxy_adapter.api.middleware.logging import LoggingMiddleware
|
18
|
+
from mcp_proxy_adapter.api.middleware.auth import AuthMiddleware
|
19
|
+
from mcp_proxy_adapter.api.middleware.rate_limit import RateLimitMiddleware
|
20
|
+
from mcp_proxy_adapter.api.middleware.error_handling import ErrorHandlingMiddleware
|
21
|
+
from mcp_proxy_adapter.api.middleware.performance import PerformanceMiddleware
|
22
|
+
from mcp_proxy_adapter.core.errors import MicroserviceError, CommandError, ValidationError
|
23
|
+
|
24
|
+
|
25
|
+
# Helper functions
|
26
|
+
@pytest.mark.asyncio
|
27
|
+
async def test_endpoint(request):
|
28
|
+
"""Test endpoint that returns a simple JSON response."""
|
29
|
+
return JSONResponse({"message": "test"})
|
30
|
+
|
31
|
+
@pytest.mark.asyncio
|
32
|
+
async def error_endpoint(request):
|
33
|
+
"""Test endpoint that raises an error."""
|
34
|
+
# Используем новый формат JSON-RPC ошибки
|
35
|
+
raise CommandError("Test error")
|
36
|
+
|
37
|
+
@pytest.mark.asyncio
|
38
|
+
async def validation_error_endpoint(request):
|
39
|
+
"""Test endpoint that raises a validation error."""
|
40
|
+
# Изменяем параметр details на data в соответствии с новой структурой
|
41
|
+
raise ValidationError("Validation error", data={"field": "error"})
|
42
|
+
|
43
|
+
@pytest.mark.asyncio
|
44
|
+
async def json_rpc_error_endpoint(request):
|
45
|
+
"""Test endpoint that emulates JSON-RPC error handling."""
|
46
|
+
# Сразу возвращаем готовый JSON-RPC ответ с ошибкой
|
47
|
+
return JSONResponse(
|
48
|
+
status_code=400,
|
49
|
+
content={
|
50
|
+
"jsonrpc": "2.0",
|
51
|
+
"error": {
|
52
|
+
"code": -32000, # JSON-RPC код ошибки
|
53
|
+
"message": "Test error",
|
54
|
+
"data": {"test": "data"}
|
55
|
+
},
|
56
|
+
"id": 1
|
57
|
+
}
|
58
|
+
)
|
59
|
+
|
60
|
+
# Test applications
|
61
|
+
def create_test_app():
|
62
|
+
"""Create a test application with routes for testing."""
|
63
|
+
routes = [
|
64
|
+
Route("/test", test_endpoint),
|
65
|
+
Route("/error", error_endpoint),
|
66
|
+
Route("/validation", validation_error_endpoint),
|
67
|
+
Route("/api/jsonrpc", json_rpc_error_endpoint),
|
68
|
+
]
|
69
|
+
return Starlette(routes=routes)
|
70
|
+
|
71
|
+
|
72
|
+
# Tests for BaseMiddleware
|
73
|
+
def test_base_middleware():
|
74
|
+
"""Test that base middleware works correctly."""
|
75
|
+
# Create a middleware that overrides methods
|
76
|
+
class TestMiddleware(BaseMiddleware):
|
77
|
+
async def before_request(self, request):
|
78
|
+
request.state.before_called = True
|
79
|
+
|
80
|
+
async def after_response(self, request, response):
|
81
|
+
response.headers["X-After-Called"] = "True"
|
82
|
+
return response
|
83
|
+
|
84
|
+
# Create app with middleware
|
85
|
+
app = create_test_app()
|
86
|
+
app.add_middleware(TestMiddleware)
|
87
|
+
|
88
|
+
# Test
|
89
|
+
client = TestClient(app)
|
90
|
+
response = client.get("/test")
|
91
|
+
|
92
|
+
# Verify
|
93
|
+
assert response.status_code == 200
|
94
|
+
assert response.headers.get("X-After-Called") == "True"
|
95
|
+
|
96
|
+
|
97
|
+
# Tests for LoggingMiddleware
|
98
|
+
def test_logging_middleware():
|
99
|
+
"""Test that logging middleware logs requests and responses."""
|
100
|
+
# Create app with middleware
|
101
|
+
app = create_test_app()
|
102
|
+
app.add_middleware(LoggingMiddleware)
|
103
|
+
|
104
|
+
# Test
|
105
|
+
with patch("mcp_proxy_adapter.api.middleware.logging.RequestLogger") as mock_request_logger:
|
106
|
+
# Настраиваем мок для RequestLogger
|
107
|
+
mock_logger_instance = MagicMock()
|
108
|
+
mock_request_logger.return_value = mock_logger_instance
|
109
|
+
|
110
|
+
client = TestClient(app)
|
111
|
+
response = client.get("/test")
|
112
|
+
|
113
|
+
# Verify
|
114
|
+
assert response.status_code == 200
|
115
|
+
assert "X-Request-ID" in response.headers
|
116
|
+
assert "X-Process-Time" in response.headers
|
117
|
+
|
118
|
+
# Check that RequestLogger was created and used
|
119
|
+
mock_request_logger.assert_called_once()
|
120
|
+
mock_logger_instance.info.assert_called()
|
121
|
+
|
122
|
+
|
123
|
+
# Tests for AuthMiddleware
|
124
|
+
def test_auth_middleware_no_api_key():
|
125
|
+
"""Test that auth middleware blocks requests without API key."""
|
126
|
+
# Create app with middleware
|
127
|
+
app = create_test_app()
|
128
|
+
app.add_middleware(AuthMiddleware, api_keys={"valid-key": "test-user"}, auth_enabled=True)
|
129
|
+
|
130
|
+
# Test
|
131
|
+
client = TestClient(app)
|
132
|
+
response = client.get("/test")
|
133
|
+
|
134
|
+
# Verify
|
135
|
+
assert response.status_code == 401
|
136
|
+
assert "API key not provided" in response.text
|
137
|
+
|
138
|
+
|
139
|
+
def test_auth_middleware_invalid_api_key():
|
140
|
+
"""Test that auth middleware blocks requests with invalid API key."""
|
141
|
+
# Create app with middleware
|
142
|
+
app = create_test_app()
|
143
|
+
app.add_middleware(AuthMiddleware, api_keys={"valid-key": "test-user"}, auth_enabled=True)
|
144
|
+
|
145
|
+
# Test
|
146
|
+
client = TestClient(app)
|
147
|
+
response = client.get("/test", headers={"X-API-Key": "invalid-key"})
|
148
|
+
|
149
|
+
# Verify
|
150
|
+
assert response.status_code == 401
|
151
|
+
assert "Invalid API key" in response.text
|
152
|
+
|
153
|
+
|
154
|
+
def test_auth_middleware_valid_api_key():
|
155
|
+
"""Test that auth middleware allows requests with valid API key."""
|
156
|
+
# Create app with middleware
|
157
|
+
app = create_test_app()
|
158
|
+
app.add_middleware(AuthMiddleware, api_keys={"valid-key": "test-user"}, auth_enabled=True)
|
159
|
+
|
160
|
+
# Test
|
161
|
+
client = TestClient(app)
|
162
|
+
response = client.get("/test", headers={"X-API-Key": "valid-key"})
|
163
|
+
|
164
|
+
# Verify
|
165
|
+
assert response.status_code == 200
|
166
|
+
|
167
|
+
|
168
|
+
def test_auth_middleware_public_path():
|
169
|
+
"""Test that auth middleware allows requests to public paths."""
|
170
|
+
# Create app with middleware
|
171
|
+
app = create_test_app()
|
172
|
+
app.add_middleware(AuthMiddleware, api_keys={"valid-key": "test-user"}, auth_enabled=True)
|
173
|
+
|
174
|
+
# Test
|
175
|
+
client = TestClient(app)
|
176
|
+
response = client.get("/docs") # Public path
|
177
|
+
|
178
|
+
# Verify
|
179
|
+
assert response.status_code == 404 # 404 because path doesn't exist, but auth should pass
|
180
|
+
|
181
|
+
|
182
|
+
def test_auth_middleware_disabled():
|
183
|
+
"""Test that auth middleware passes requests when disabled."""
|
184
|
+
# Create app with middleware but with auth_enabled=False
|
185
|
+
app = create_test_app()
|
186
|
+
app.add_middleware(AuthMiddleware, api_keys={"valid-key": "test-user"}, auth_enabled=False)
|
187
|
+
|
188
|
+
# Test
|
189
|
+
client = TestClient(app)
|
190
|
+
response = client.get("/test") # No API key provided
|
191
|
+
|
192
|
+
# Verify
|
193
|
+
assert response.status_code == 200 # Should pass because auth is disabled
|
194
|
+
|
195
|
+
|
196
|
+
# Tests for RateLimitMiddleware
|
197
|
+
def test_rate_limit_middleware_exceeds_limit():
|
198
|
+
"""Test that rate limit middleware blocks requests when limit is exceeded."""
|
199
|
+
# Create app with middleware (low limit for testing)
|
200
|
+
app = create_test_app()
|
201
|
+
app.add_middleware(RateLimitMiddleware, rate_limit=2, time_window=60)
|
202
|
+
|
203
|
+
# Test
|
204
|
+
client = TestClient(app)
|
205
|
+
|
206
|
+
# First two requests should pass
|
207
|
+
response1 = client.get("/test")
|
208
|
+
response2 = client.get("/test")
|
209
|
+
|
210
|
+
# Third request should be rate limited
|
211
|
+
response3 = client.get("/test")
|
212
|
+
|
213
|
+
# Verify
|
214
|
+
assert response1.status_code == 200
|
215
|
+
assert response2.status_code == 200
|
216
|
+
assert response3.status_code == 429
|
217
|
+
assert "Rate limit exceeded" in response3.text
|
218
|
+
|
219
|
+
|
220
|
+
def test_rate_limit_middleware_public_path():
|
221
|
+
"""Test that rate limit middleware allows requests to public paths regardless of limit."""
|
222
|
+
# Create app with middleware (low limit for testing)
|
223
|
+
app = create_test_app()
|
224
|
+
app.add_middleware(RateLimitMiddleware, rate_limit=1, time_window=60)
|
225
|
+
|
226
|
+
# Test
|
227
|
+
client = TestClient(app)
|
228
|
+
|
229
|
+
# First request to normal path should pass
|
230
|
+
response1 = client.get("/test")
|
231
|
+
|
232
|
+
# Second request to normal path should be rate limited
|
233
|
+
response2 = client.get("/test")
|
234
|
+
|
235
|
+
# Request to public path should pass despite rate limit
|
236
|
+
response3 = client.get("/health") # Public path
|
237
|
+
|
238
|
+
# Verify
|
239
|
+
assert response1.status_code == 200
|
240
|
+
assert response2.status_code == 429
|
241
|
+
assert response3.status_code == 404 # 404 because path doesn't exist, but rate limit should pass
|
242
|
+
|
243
|
+
|
244
|
+
# Tests for ErrorHandlingMiddleware
|
245
|
+
def test_error_handling_middleware_command_error():
|
246
|
+
"""Test that error handling middleware formats command errors correctly."""
|
247
|
+
# Create app with middleware
|
248
|
+
app = create_test_app()
|
249
|
+
app.add_middleware(ErrorHandlingMiddleware)
|
250
|
+
|
251
|
+
# Test
|
252
|
+
client = TestClient(app)
|
253
|
+
response = client.get("/error")
|
254
|
+
|
255
|
+
# Verify
|
256
|
+
assert response.status_code == 400 # ErrorHandlingMiddleware возвращает 400 для CommandError
|
257
|
+
result = response.json()
|
258
|
+
# В новом формате JSON-RPC мы возвращаем непосредственно объект с code и message
|
259
|
+
assert "code" in result
|
260
|
+
assert "message" in result
|
261
|
+
assert result["code"] == -32000 # Код ошибки JSON-RPC
|
262
|
+
assert result["message"] == "Test error"
|
263
|
+
|
264
|
+
|
265
|
+
def test_error_handling_middleware_validation_error():
|
266
|
+
"""Test that error handling middleware formats validation errors correctly."""
|
267
|
+
# Create app with middleware
|
268
|
+
app = create_test_app()
|
269
|
+
app.add_middleware(ErrorHandlingMiddleware)
|
270
|
+
|
271
|
+
# Test
|
272
|
+
client = TestClient(app)
|
273
|
+
response = client.get("/validation")
|
274
|
+
|
275
|
+
# Verify
|
276
|
+
assert response.status_code == 400
|
277
|
+
result = response.json()
|
278
|
+
# В новом формате JSON-RPC мы возвращаем непосредственно объект с code и message
|
279
|
+
assert "code" in result
|
280
|
+
assert "message" in result
|
281
|
+
assert "data" in result
|
282
|
+
assert result["code"] == -32602 # Код InvalidParams JSON-RPC
|
283
|
+
assert result["message"] == "Validation error"
|
284
|
+
assert result["data"]["field"] == "error"
|
285
|
+
|
286
|
+
|
287
|
+
def test_error_handling_middleware_jsonrpc_error():
|
288
|
+
"""Test that error handling middleware formats JSON-RPC errors correctly."""
|
289
|
+
# Для этого теста мы используем прямой запрос к эндпоинту, который
|
290
|
+
# возвращает заранее сформированный JSON-RPC ответ с ошибкой
|
291
|
+
app = create_test_app()
|
292
|
+
client = TestClient(app)
|
293
|
+
|
294
|
+
# Выполняем запрос к JSON-RPC эндпоинту
|
295
|
+
response = client.get("/api/jsonrpc")
|
296
|
+
|
297
|
+
# Verify
|
298
|
+
assert response.status_code == 400
|
299
|
+
assert response.json()["jsonrpc"] == "2.0"
|
300
|
+
assert "error" in response.json()
|
301
|
+
assert response.json()["error"]["code"] == -32000 # Обновленный код JSON-RPC
|
302
|
+
assert response.json()["error"]["message"] == "Test error"
|
303
|
+
assert response.json()["error"]["data"] == {"test": "data"} # data вместо details
|
304
|
+
assert response.json()["id"] == 1
|
305
|
+
|
306
|
+
|
307
|
+
# Tests for PerformanceMiddleware
|
308
|
+
@pytest.mark.asyncio
|
309
|
+
async def test_performance_middleware():
|
310
|
+
"""Test that performance middleware tracks request times."""
|
311
|
+
# Создаем middleware напрямую для тестирования
|
312
|
+
middleware = PerformanceMiddleware(None)
|
313
|
+
|
314
|
+
# Создаем мок для запроса
|
315
|
+
mock_request = MagicMock()
|
316
|
+
mock_request.url.path = "/test"
|
317
|
+
|
318
|
+
# Создаем мок для call_next
|
319
|
+
mock_response = JSONResponse({"message": "test"})
|
320
|
+
|
321
|
+
async def mock_call_next(request):
|
322
|
+
return mock_response
|
323
|
+
|
324
|
+
# Симуляция нескольких запросов без использования кастомного event loop
|
325
|
+
for _ in range(5):
|
326
|
+
response = await middleware.dispatch(mock_request, mock_call_next)
|
327
|
+
assert response == mock_response
|
328
|
+
|
329
|
+
# Проверка, что времена запросов сохранены
|
330
|
+
assert "/test" in middleware.request_times
|
331
|
+
assert len(middleware.request_times["/test"]) == 5
|
332
|
+
|
333
|
+
# Тестируем метод логирования статистики
|
334
|
+
with patch("mcp_proxy_adapter.api.middleware.performance.logger") as mock_logger:
|
335
|
+
middleware._log_stats()
|
336
|
+
mock_logger.info.assert_called()
|
@@ -0,0 +1,211 @@
|
|
1
|
+
"""
|
2
|
+
Unit tests for config command.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import os
|
7
|
+
import tempfile
|
8
|
+
from typing import Generator
|
9
|
+
|
10
|
+
import pytest
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.commands.config_command import ConfigCommand, ConfigResult
|
13
|
+
from mcp_proxy_adapter.config import Config
|
14
|
+
|
15
|
+
|
16
|
+
@pytest.fixture
|
17
|
+
def temp_config_file() -> Generator[str, None, None]:
|
18
|
+
"""
|
19
|
+
Creates temporary configuration file for tests.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
Path to temporary configuration file.
|
23
|
+
"""
|
24
|
+
# Create temporary file
|
25
|
+
fd, path = tempfile.mkstemp(suffix=".json")
|
26
|
+
|
27
|
+
# Write test configuration
|
28
|
+
test_config = {
|
29
|
+
"server": {
|
30
|
+
"host": "127.0.0.1",
|
31
|
+
"port": 8000
|
32
|
+
},
|
33
|
+
"logging": {
|
34
|
+
"level": "DEBUG",
|
35
|
+
"file": "test.log"
|
36
|
+
},
|
37
|
+
"test_section": {
|
38
|
+
"test_key": "test_value",
|
39
|
+
"nested": {
|
40
|
+
"key1": "value1",
|
41
|
+
"key2": 42
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
with os.fdopen(fd, "w") as f:
|
47
|
+
json.dump(test_config, f)
|
48
|
+
|
49
|
+
yield path
|
50
|
+
|
51
|
+
# Remove temporary file after tests
|
52
|
+
os.unlink(path)
|
53
|
+
|
54
|
+
|
55
|
+
@pytest.mark.unit
|
56
|
+
async def test_config_command_get_all(temp_config_file: str):
|
57
|
+
"""
|
58
|
+
Test getting all configuration values.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
temp_config_file: Path to temporary configuration file.
|
62
|
+
"""
|
63
|
+
# Create config instance with test file
|
64
|
+
config_instance = Config(temp_config_file)
|
65
|
+
|
66
|
+
# Create command with this config
|
67
|
+
command = ConfigCommand()
|
68
|
+
|
69
|
+
# Override the config instance used in the command
|
70
|
+
from mcp_proxy_adapter.commands import config_command
|
71
|
+
original_config = config_command.config_instance
|
72
|
+
config_command.config_instance = config_instance
|
73
|
+
|
74
|
+
try:
|
75
|
+
# Execute command with get operation and no path
|
76
|
+
result = await command.execute(operation="get")
|
77
|
+
|
78
|
+
# Check result
|
79
|
+
assert isinstance(result, ConfigResult)
|
80
|
+
result_dict = result.to_dict()
|
81
|
+
assert result_dict["success"] is True
|
82
|
+
assert "data" in result_dict
|
83
|
+
assert "config" in result_dict["data"]
|
84
|
+
assert "operation" in result_dict["data"]
|
85
|
+
assert result_dict["data"]["operation"] == "get"
|
86
|
+
|
87
|
+
# Check all config values are present
|
88
|
+
config_data = result_dict["data"]["config"]
|
89
|
+
assert "server" in config_data
|
90
|
+
assert "logging" in config_data
|
91
|
+
assert "test_section" in config_data
|
92
|
+
|
93
|
+
assert config_data["server"]["host"] == "127.0.0.1"
|
94
|
+
assert config_data["logging"]["level"] == "DEBUG"
|
95
|
+
assert config_data["test_section"]["test_key"] == "test_value"
|
96
|
+
finally:
|
97
|
+
# Restore original config instance
|
98
|
+
config_command.config_instance = original_config
|
99
|
+
|
100
|
+
|
101
|
+
@pytest.mark.unit
|
102
|
+
async def test_config_command_get_specific(temp_config_file: str):
|
103
|
+
"""
|
104
|
+
Test getting specific configuration value.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
temp_config_file: Path to temporary configuration file.
|
108
|
+
"""
|
109
|
+
# Create config instance with test file
|
110
|
+
config_instance = Config(temp_config_file)
|
111
|
+
|
112
|
+
# Create command with this config
|
113
|
+
command = ConfigCommand()
|
114
|
+
|
115
|
+
# Override the config instance used in the command
|
116
|
+
from mcp_proxy_adapter.commands import config_command
|
117
|
+
original_config = config_command.config_instance
|
118
|
+
config_command.config_instance = config_instance
|
119
|
+
|
120
|
+
try:
|
121
|
+
# Execute command with get operation and specific path
|
122
|
+
result = await command.execute(operation="get", path="server.host")
|
123
|
+
|
124
|
+
# Check result
|
125
|
+
assert isinstance(result, ConfigResult)
|
126
|
+
result_dict = result.to_dict()
|
127
|
+
assert result_dict["success"] is True
|
128
|
+
assert "data" in result_dict
|
129
|
+
assert "config" in result_dict["data"]
|
130
|
+
assert "operation" in result_dict["data"]
|
131
|
+
assert result_dict["data"]["operation"] == "get"
|
132
|
+
|
133
|
+
# Check specific config value
|
134
|
+
config_data = result_dict["data"]["config"]
|
135
|
+
assert "server.host" in config_data
|
136
|
+
assert config_data["server.host"] == "127.0.0.1"
|
137
|
+
finally:
|
138
|
+
# Restore original config instance
|
139
|
+
config_command.config_instance = original_config
|
140
|
+
|
141
|
+
|
142
|
+
@pytest.mark.unit
|
143
|
+
async def test_config_command_set_value(temp_config_file: str):
|
144
|
+
"""
|
145
|
+
Test setting configuration value.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
temp_config_file: Path to temporary configuration file.
|
149
|
+
"""
|
150
|
+
# Create config instance with test file
|
151
|
+
config_instance = Config(temp_config_file)
|
152
|
+
|
153
|
+
# Create command with this config
|
154
|
+
command = ConfigCommand()
|
155
|
+
|
156
|
+
# Override the config instance used in the command
|
157
|
+
from mcp_proxy_adapter.commands import config_command
|
158
|
+
original_config = config_command.config_instance
|
159
|
+
config_command.config_instance = config_instance
|
160
|
+
|
161
|
+
try:
|
162
|
+
# Execute command with set operation
|
163
|
+
result = await command.execute(
|
164
|
+
operation="set",
|
165
|
+
path="server.host",
|
166
|
+
value="localhost"
|
167
|
+
)
|
168
|
+
|
169
|
+
# Check result
|
170
|
+
assert isinstance(result, ConfigResult)
|
171
|
+
result_dict = result.to_dict()
|
172
|
+
assert result_dict["success"] is True
|
173
|
+
assert "data" in result_dict
|
174
|
+
assert "config" in result_dict["data"]
|
175
|
+
assert "operation" in result_dict["data"]
|
176
|
+
assert result_dict["data"]["operation"] == "set"
|
177
|
+
|
178
|
+
# Check updated config value
|
179
|
+
config_data = result_dict["data"]["config"]
|
180
|
+
assert "server.host" in config_data
|
181
|
+
assert config_data["server.host"] == "localhost"
|
182
|
+
|
183
|
+
# Check that value was updated in config instance
|
184
|
+
assert config_instance.get("server.host") == "localhost"
|
185
|
+
finally:
|
186
|
+
# Restore original config instance
|
187
|
+
config_command.config_instance = original_config
|
188
|
+
|
189
|
+
|
190
|
+
@pytest.mark.unit
|
191
|
+
async def test_config_command_validate_schema():
|
192
|
+
"""
|
193
|
+
Test validation schema for config command.
|
194
|
+
"""
|
195
|
+
command = ConfigCommand()
|
196
|
+
schema = command.get_schema()
|
197
|
+
|
198
|
+
# Check schema structure
|
199
|
+
assert schema["type"] == "object"
|
200
|
+
assert "properties" in schema
|
201
|
+
assert "operation" in schema["properties"]
|
202
|
+
assert "path" in schema["properties"]
|
203
|
+
assert "value" in schema["properties"]
|
204
|
+
|
205
|
+
# Check operation property
|
206
|
+
operation_prop = schema["properties"]["operation"]
|
207
|
+
assert operation_prop["type"] == "string"
|
208
|
+
assert "enum" in operation_prop
|
209
|
+
assert "get" in operation_prop["enum"]
|
210
|
+
assert "set" in operation_prop["enum"]
|
211
|
+
assert operation_prop["default"] == "get"
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"""
|
2
|
+
Tests for the echo command.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
import asyncio
|
7
|
+
from typing import Dict, Any
|
8
|
+
import json
|
9
|
+
|
10
|
+
from mcp_proxy_adapter.tests.stubs.echo_command import EchoCommand
|
11
|
+
from mcp_proxy_adapter.tests.stubs.echo_command import EchoResult
|
12
|
+
|
13
|
+
|
14
|
+
@pytest.mark.unit
|
15
|
+
def test_echo_command_execution():
|
16
|
+
"""
|
17
|
+
Test execution of echo command.
|
18
|
+
"""
|
19
|
+
# Create test parameters
|
20
|
+
test_params = {
|
21
|
+
"string_param": "test_value",
|
22
|
+
"int_param": 42,
|
23
|
+
"bool_param": True,
|
24
|
+
"complex_param": {"nested": "value", "array": [1, 2, 3]}
|
25
|
+
}
|
26
|
+
|
27
|
+
# Create and execute command
|
28
|
+
command = EchoCommand()
|
29
|
+
result = asyncio.run(command.execute(**test_params))
|
30
|
+
|
31
|
+
# Check result type
|
32
|
+
assert isinstance(result, EchoResult)
|
33
|
+
|
34
|
+
# Check result content
|
35
|
+
assert result.params == test_params
|
36
|
+
assert result.params["string_param"] == "test_value"
|
37
|
+
assert result.params["int_param"] == 42
|
38
|
+
assert result.params["bool_param"] is True
|
39
|
+
assert result.params["complex_param"]["nested"] == "value"
|
40
|
+
assert result.params["complex_param"]["array"] == [1, 2, 3]
|
41
|
+
|
42
|
+
|
43
|
+
@pytest.mark.unit
|
44
|
+
def test_echo_result_serialization():
|
45
|
+
"""
|
46
|
+
Test serialization of echo result.
|
47
|
+
"""
|
48
|
+
# Create test parameters
|
49
|
+
test_params = {
|
50
|
+
"string_param": "test_value",
|
51
|
+
"int_param": 42,
|
52
|
+
"bool_param": True,
|
53
|
+
"complex_param": {"nested": "value", "array": [1, 2, 3]}
|
54
|
+
}
|
55
|
+
|
56
|
+
# Create result
|
57
|
+
result = EchoResult(params=test_params)
|
58
|
+
|
59
|
+
# Test to_dict method
|
60
|
+
result_dict = result.to_dict()
|
61
|
+
assert isinstance(result_dict, dict)
|
62
|
+
assert "params" in result_dict
|
63
|
+
assert result_dict["params"] == test_params
|
64
|
+
|
65
|
+
# Test that result can be properly serialized to JSON
|
66
|
+
json_str = json.dumps(result_dict)
|
67
|
+
parsed_json = json.loads(json_str)
|
68
|
+
assert parsed_json == result_dict
|
69
|
+
|
70
|
+
|
71
|
+
@pytest.mark.unit
|
72
|
+
def test_echo_command_schema():
|
73
|
+
"""
|
74
|
+
Test command schema generation.
|
75
|
+
"""
|
76
|
+
# Get schema
|
77
|
+
schema = EchoCommand.get_schema()
|
78
|
+
|
79
|
+
# Check schema structure
|
80
|
+
assert isinstance(schema, dict)
|
81
|
+
assert "type" in schema and schema["type"] == "object"
|
82
|
+
assert "additionalProperties" in schema and schema["additionalProperties"] is True
|
83
|
+
assert "description" in schema
|
84
|
+
|
85
|
+
|
86
|
+
@pytest.mark.unit
|
87
|
+
def test_echo_result_schema():
|
88
|
+
"""
|
89
|
+
Test result schema generation.
|
90
|
+
"""
|
91
|
+
# Get schema
|
92
|
+
schema = EchoResult.get_schema()
|
93
|
+
|
94
|
+
# Check schema structure
|
95
|
+
assert isinstance(schema, dict)
|
96
|
+
assert "type" in schema and schema["type"] == "object"
|
97
|
+
assert "properties" in schema
|
98
|
+
assert "params" in schema["properties"]
|
99
|
+
assert "required" in schema and "params" in schema["required"]
|
100
|
+
assert schema["properties"]["params"]["type"] == "object"
|
101
|
+
assert schema["properties"]["params"]["additionalProperties"] is True
|
102
|
+
|
103
|
+
|
104
|
+
@pytest.mark.unit
|
105
|
+
def test_echo_result_from_dict():
|
106
|
+
"""
|
107
|
+
Test creating result from dictionary.
|
108
|
+
"""
|
109
|
+
# Create test data
|
110
|
+
test_data = {
|
111
|
+
"params": {
|
112
|
+
"key1": "value1",
|
113
|
+
"key2": 42
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
# Create result from dict
|
118
|
+
result = EchoResult.from_dict(test_data)
|
119
|
+
|
120
|
+
# Check result
|
121
|
+
assert isinstance(result, EchoResult)
|
122
|
+
assert result.params == test_data["params"]
|
123
|
+
|
124
|
+
# Test with empty params
|
125
|
+
empty_result = EchoResult.from_dict({})
|
126
|
+
assert isinstance(empty_result, EchoResult)
|
127
|
+
assert empty_result.params == {}
|