mcp-proxy-adapter 2.1.17__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.
Files changed (135) hide show
  1. examples/__init__.py +19 -0
  2. examples/anti_patterns/README.md +51 -0
  3. examples/anti_patterns/__init__.py +9 -0
  4. examples/anti_patterns/bad_design/README.md +72 -0
  5. examples/anti_patterns/bad_design/global_state.py +170 -0
  6. examples/anti_patterns/bad_design/monolithic_command.py +272 -0
  7. examples/basic_example/README.md +245 -0
  8. examples/basic_example/__init__.py +8 -0
  9. examples/basic_example/commands/__init__.py +5 -0
  10. examples/basic_example/commands/echo_command.py +95 -0
  11. examples/basic_example/commands/math_command.py +151 -0
  12. examples/basic_example/commands/time_command.py +152 -0
  13. examples/basic_example/config.json +25 -0
  14. examples/basic_example/docs/EN/README.md +177 -0
  15. examples/basic_example/docs/RU/README.md +177 -0
  16. examples/basic_example/server.py +151 -0
  17. examples/basic_example/tests/conftest.py +243 -0
  18. examples/commands/echo_command.py +52 -0
  19. examples/commands/echo_result.py +65 -0
  20. examples/commands/get_date_command.py +98 -0
  21. examples/commands/new_uuid4_command.py +91 -0
  22. examples/complete_example/Dockerfile +24 -0
  23. examples/complete_example/README.md +92 -0
  24. examples/complete_example/__init__.py +8 -0
  25. examples/complete_example/commands/__init__.py +5 -0
  26. examples/complete_example/commands/system_command.py +328 -0
  27. examples/complete_example/config.json +41 -0
  28. examples/complete_example/configs/config.dev.yaml +40 -0
  29. examples/complete_example/configs/config.docker.yaml +40 -0
  30. examples/complete_example/docker-compose.yml +35 -0
  31. examples/complete_example/requirements.txt +20 -0
  32. examples/complete_example/server.py +139 -0
  33. examples/minimal_example/README.md +65 -0
  34. examples/minimal_example/__init__.py +8 -0
  35. examples/minimal_example/config.json +14 -0
  36. examples/minimal_example/main.py +136 -0
  37. examples/minimal_example/simple_server.py +163 -0
  38. examples/minimal_example/tests/conftest.py +171 -0
  39. examples/minimal_example/tests/test_hello_command.py +111 -0
  40. examples/minimal_example/tests/test_integration.py +181 -0
  41. examples/server.py +69 -0
  42. examples/simple_server.py +128 -0
  43. examples/test_server.py +134 -0
  44. examples/tool_description_example.py +82 -0
  45. mcp_proxy_adapter/__init__.py +33 -1
  46. mcp_proxy_adapter/api/__init__.py +0 -0
  47. mcp_proxy_adapter/api/app.py +391 -0
  48. mcp_proxy_adapter/api/handlers.py +229 -0
  49. mcp_proxy_adapter/api/middleware/__init__.py +49 -0
  50. mcp_proxy_adapter/api/middleware/auth.py +146 -0
  51. mcp_proxy_adapter/api/middleware/base.py +79 -0
  52. mcp_proxy_adapter/api/middleware/error_handling.py +198 -0
  53. mcp_proxy_adapter/api/middleware/logging.py +96 -0
  54. mcp_proxy_adapter/api/middleware/performance.py +83 -0
  55. mcp_proxy_adapter/api/middleware/rate_limit.py +152 -0
  56. mcp_proxy_adapter/api/schemas.py +305 -0
  57. mcp_proxy_adapter/api/tool_integration.py +223 -0
  58. mcp_proxy_adapter/api/tools.py +198 -0
  59. mcp_proxy_adapter/commands/__init__.py +19 -0
  60. mcp_proxy_adapter/commands/base.py +301 -0
  61. mcp_proxy_adapter/commands/command_registry.py +231 -0
  62. mcp_proxy_adapter/commands/config_command.py +113 -0
  63. mcp_proxy_adapter/commands/health_command.py +136 -0
  64. mcp_proxy_adapter/commands/help_command.py +193 -0
  65. mcp_proxy_adapter/commands/result.py +215 -0
  66. mcp_proxy_adapter/config.py +195 -0
  67. mcp_proxy_adapter/core/__init__.py +0 -0
  68. mcp_proxy_adapter/core/errors.py +173 -0
  69. mcp_proxy_adapter/core/logging.py +205 -0
  70. mcp_proxy_adapter/core/utils.py +138 -0
  71. mcp_proxy_adapter/custom_openapi.py +125 -0
  72. mcp_proxy_adapter/openapi.py +403 -0
  73. mcp_proxy_adapter/py.typed +0 -0
  74. mcp_proxy_adapter/schemas/base_schema.json +114 -0
  75. mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
  76. mcp_proxy_adapter/tests/__init__.py +0 -0
  77. mcp_proxy_adapter/tests/api/__init__.py +3 -0
  78. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +115 -0
  79. mcp_proxy_adapter/tests/api/test_middleware.py +336 -0
  80. mcp_proxy_adapter/tests/commands/__init__.py +3 -0
  81. mcp_proxy_adapter/tests/commands/test_config_command.py +211 -0
  82. mcp_proxy_adapter/tests/commands/test_echo_command.py +127 -0
  83. mcp_proxy_adapter/tests/commands/test_help_command.py +133 -0
  84. mcp_proxy_adapter/tests/conftest.py +131 -0
  85. mcp_proxy_adapter/tests/functional/__init__.py +3 -0
  86. mcp_proxy_adapter/tests/functional/test_api.py +235 -0
  87. mcp_proxy_adapter/tests/integration/__init__.py +3 -0
  88. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +130 -0
  89. mcp_proxy_adapter/tests/integration/test_integration.py +255 -0
  90. mcp_proxy_adapter/tests/performance/__init__.py +3 -0
  91. mcp_proxy_adapter/tests/performance/test_performance.py +189 -0
  92. mcp_proxy_adapter/tests/stubs/__init__.py +10 -0
  93. mcp_proxy_adapter/tests/stubs/echo_command.py +104 -0
  94. mcp_proxy_adapter/tests/test_api_endpoints.py +271 -0
  95. mcp_proxy_adapter/tests/test_api_handlers.py +289 -0
  96. mcp_proxy_adapter/tests/test_base_command.py +123 -0
  97. mcp_proxy_adapter/tests/test_batch_requests.py +117 -0
  98. mcp_proxy_adapter/tests/test_command_registry.py +245 -0
  99. mcp_proxy_adapter/tests/test_config.py +127 -0
  100. mcp_proxy_adapter/tests/test_utils.py +65 -0
  101. mcp_proxy_adapter/tests/unit/__init__.py +3 -0
  102. mcp_proxy_adapter/tests/unit/test_base_command.py +130 -0
  103. mcp_proxy_adapter/tests/unit/test_config.py +217 -0
  104. mcp_proxy_adapter/version.py +3 -0
  105. mcp_proxy_adapter-3.0.1.dist-info/METADATA +200 -0
  106. mcp_proxy_adapter-3.0.1.dist-info/RECORD +109 -0
  107. {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/top_level.txt +1 -0
  108. mcp_proxy_adapter/adapter.py +0 -697
  109. mcp_proxy_adapter/analyzers/__init__.py +0 -1
  110. mcp_proxy_adapter/analyzers/docstring_analyzer.py +0 -199
  111. mcp_proxy_adapter/analyzers/type_analyzer.py +0 -151
  112. mcp_proxy_adapter/dispatchers/__init__.py +0 -1
  113. mcp_proxy_adapter/dispatchers/base_dispatcher.py +0 -85
  114. mcp_proxy_adapter/dispatchers/json_rpc_dispatcher.py +0 -262
  115. mcp_proxy_adapter/examples/analyze_config.py +0 -141
  116. mcp_proxy_adapter/examples/basic_integration.py +0 -155
  117. mcp_proxy_adapter/examples/docstring_and_schema_example.py +0 -69
  118. mcp_proxy_adapter/examples/extension_example.py +0 -72
  119. mcp_proxy_adapter/examples/help_best_practices.py +0 -67
  120. mcp_proxy_adapter/examples/help_usage.py +0 -64
  121. mcp_proxy_adapter/examples/mcp_proxy_client.py +0 -131
  122. mcp_proxy_adapter/examples/openapi_server.py +0 -383
  123. mcp_proxy_adapter/examples/project_structure_example.py +0 -47
  124. mcp_proxy_adapter/examples/testing_example.py +0 -64
  125. mcp_proxy_adapter/models.py +0 -47
  126. mcp_proxy_adapter/registry.py +0 -439
  127. mcp_proxy_adapter/schema.py +0 -257
  128. mcp_proxy_adapter/testing_utils.py +0 -112
  129. mcp_proxy_adapter/validators/__init__.py +0 -1
  130. mcp_proxy_adapter/validators/docstring_validator.py +0 -75
  131. mcp_proxy_adapter/validators/metadata_validator.py +0 -76
  132. mcp_proxy_adapter-2.1.17.dist-info/METADATA +0 -376
  133. mcp_proxy_adapter-2.1.17.dist-info/RECORD +0 -30
  134. {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/WHEEL +0 -0
  135. {mcp_proxy_adapter-2.1.17.dist-info → mcp_proxy_adapter-3.0.1.dist-info}/licenses/LICENSE +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,3 @@
1
+ """
2
+ Tests for commands package.
3
+ """
@@ -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 == {}