mockserver-client 5.16.0__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.
- mockserver/__init__.py +85 -0
- mockserver/async_client.py +425 -0
- mockserver/client.py +207 -0
- mockserver/exceptions.py +21 -0
- mockserver/fluent.py +116 -0
- mockserver/models.py +1358 -0
- mockserver/websocket_client.py +314 -0
- mockserver_client-5.16.0.dist-info/METADATA +291 -0
- mockserver_client-5.16.0.dist-info/RECORD +11 -0
- mockserver_client-5.16.0.dist-info/WHEEL +5 -0
- mockserver_client-5.16.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import websockets
|
|
10
|
+
import websockets.client
|
|
11
|
+
import websockets.exceptions
|
|
12
|
+
|
|
13
|
+
from mockserver.exceptions import MockServerCallbackError, MockServerWebSocketError
|
|
14
|
+
from mockserver.models import (
|
|
15
|
+
HttpRequest,
|
|
16
|
+
HttpRequestAndHttpResponse,
|
|
17
|
+
HttpResponse,
|
|
18
|
+
KeyToMultiValue,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
WEB_SOCKET_CORRELATION_ID_HEADER_NAME = "WebSocketCorrelationId"
|
|
24
|
+
CLIENT_REGISTRATION_ID_HEADER = "X-CLIENT-REGISTRATION-ID"
|
|
25
|
+
|
|
26
|
+
WEBSOCKET_PATH = "/_mockserver_callback_websocket"
|
|
27
|
+
|
|
28
|
+
TYPE_HTTP_REQUEST = "org.mockserver.model.HttpRequest"
|
|
29
|
+
TYPE_HTTP_RESPONSE = "org.mockserver.model.HttpResponse"
|
|
30
|
+
TYPE_HTTP_REQUEST_AND_RESPONSE = "org.mockserver.model.HttpRequestAndHttpResponse"
|
|
31
|
+
TYPE_CLIENT_ID_DTO = "org.mockserver.serialization.model.WebSocketClientIdDTO"
|
|
32
|
+
TYPE_ERROR_DTO = "org.mockserver.serialization.model.WebSocketErrorDTO"
|
|
33
|
+
|
|
34
|
+
MAX_RECONNECT_ATTEMPTS = 3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_correlation_id(request: HttpRequest) -> str | None:
|
|
38
|
+
if request.headers is None:
|
|
39
|
+
return None
|
|
40
|
+
for header in request.headers:
|
|
41
|
+
if header.name == WEB_SOCKET_CORRELATION_ID_HEADER_NAME:
|
|
42
|
+
if header.values:
|
|
43
|
+
return header.values[0]
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _add_correlation_id_header(
|
|
48
|
+
message: HttpResponse | HttpRequest, correlation_id: str
|
|
49
|
+
) -> HttpResponse | HttpRequest:
|
|
50
|
+
if message.headers is None:
|
|
51
|
+
message.headers = []
|
|
52
|
+
for header in message.headers:
|
|
53
|
+
if header.name == WEB_SOCKET_CORRELATION_ID_HEADER_NAME:
|
|
54
|
+
header.values = [correlation_id]
|
|
55
|
+
return message
|
|
56
|
+
message.headers.append(
|
|
57
|
+
KeyToMultiValue(name=WEB_SOCKET_CORRELATION_ID_HEADER_NAME, values=[correlation_id])
|
|
58
|
+
)
|
|
59
|
+
return message
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_ws_message(type_name: str, value_dict: dict) -> str:
|
|
63
|
+
return json.dumps({
|
|
64
|
+
"type": type_name,
|
|
65
|
+
"value": json.dumps(value_dict),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_error_message(error_msg: str, correlation_id: str) -> str:
|
|
70
|
+
return json.dumps({
|
|
71
|
+
"type": TYPE_ERROR_DTO,
|
|
72
|
+
"value": json.dumps({
|
|
73
|
+
"message": error_msg,
|
|
74
|
+
"webSocketCorrelationId": correlation_id,
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _clean_context_path(context_path: str) -> str:
|
|
80
|
+
if not context_path:
|
|
81
|
+
return ""
|
|
82
|
+
if not context_path.startswith("/"):
|
|
83
|
+
return "/" + context_path
|
|
84
|
+
return context_path
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MockServerWebSocketClient:
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self._ws: websockets.client.WebSocketClientProtocol | None = None
|
|
90
|
+
self._client_id: str | None = None
|
|
91
|
+
self._response_callback: Callable | None = None
|
|
92
|
+
self._forward_callback: Callable | None = None
|
|
93
|
+
self._forward_response_callback: Callable | None = None
|
|
94
|
+
self._stopped = False
|
|
95
|
+
self._listen_task: asyncio.Task | None = None
|
|
96
|
+
self._host: str = ""
|
|
97
|
+
self._port: int = 0
|
|
98
|
+
self._context_path: str = ""
|
|
99
|
+
self._secure: bool = False
|
|
100
|
+
self._ssl_context: Any = None
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def is_connected(self) -> bool:
|
|
104
|
+
return self._ws is not None and self._ws.open
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def client_id(self) -> str | None:
|
|
108
|
+
return self._client_id
|
|
109
|
+
|
|
110
|
+
async def connect(
|
|
111
|
+
self,
|
|
112
|
+
host: str,
|
|
113
|
+
port: int,
|
|
114
|
+
context_path: str = "",
|
|
115
|
+
secure: bool = False,
|
|
116
|
+
ca_cert_path: str | None = None,
|
|
117
|
+
client_id: str | None = None,
|
|
118
|
+
tls_verify: bool = True,
|
|
119
|
+
) -> str:
|
|
120
|
+
self._host = host
|
|
121
|
+
self._port = port
|
|
122
|
+
self._context_path = context_path
|
|
123
|
+
self._secure = secure
|
|
124
|
+
|
|
125
|
+
if secure:
|
|
126
|
+
import ssl
|
|
127
|
+
self._ssl_context = ssl.create_default_context()
|
|
128
|
+
if ca_cert_path:
|
|
129
|
+
self._ssl_context.load_verify_locations(ca_cert_path)
|
|
130
|
+
elif not tls_verify:
|
|
131
|
+
self._ssl_context.check_hostname = False
|
|
132
|
+
self._ssl_context.verify_mode = ssl.CERT_NONE
|
|
133
|
+
|
|
134
|
+
registration_id = client_id or str(uuid.uuid4())
|
|
135
|
+
|
|
136
|
+
await self._do_connect(registration_id)
|
|
137
|
+
return self._client_id
|
|
138
|
+
|
|
139
|
+
async def _do_connect(self, registration_id: str) -> None:
|
|
140
|
+
scheme = "wss" if self._secure else "ws"
|
|
141
|
+
path = _clean_context_path(self._context_path) + WEBSOCKET_PATH
|
|
142
|
+
uri = f"{scheme}://{self._host}:{self._port}{path}"
|
|
143
|
+
|
|
144
|
+
extra_headers = {CLIENT_REGISTRATION_ID_HEADER: registration_id}
|
|
145
|
+
|
|
146
|
+
self._ws = await websockets.client.connect(
|
|
147
|
+
uri,
|
|
148
|
+
additional_headers=extra_headers,
|
|
149
|
+
ssl=self._ssl_context if self._secure else None,
|
|
150
|
+
open_timeout=10,
|
|
151
|
+
ping_interval=20,
|
|
152
|
+
ping_timeout=20,
|
|
153
|
+
close_timeout=5,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
registration_msg = await asyncio.wait_for(self._ws.recv(), timeout=10.0)
|
|
157
|
+
parsed = json.loads(registration_msg)
|
|
158
|
+
if parsed.get("type") == TYPE_CLIENT_ID_DTO:
|
|
159
|
+
value = json.loads(parsed["value"])
|
|
160
|
+
self._client_id = value["clientId"]
|
|
161
|
+
else:
|
|
162
|
+
raise MockServerWebSocketError(
|
|
163
|
+
f"Expected WebSocketClientIdDTO but received: {parsed.get('type')}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def register_response_callback(self, callback_fn: Callable) -> None:
|
|
167
|
+
self._response_callback = callback_fn
|
|
168
|
+
|
|
169
|
+
def register_forward_callback(
|
|
170
|
+
self,
|
|
171
|
+
forward_fn: Callable,
|
|
172
|
+
response_fn: Callable | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
self._forward_callback = forward_fn
|
|
175
|
+
self._forward_response_callback = response_fn
|
|
176
|
+
|
|
177
|
+
async def listen(self) -> None:
|
|
178
|
+
reconnect_attempts = 0
|
|
179
|
+
while not self._stopped:
|
|
180
|
+
try:
|
|
181
|
+
async for raw_message in self._ws:
|
|
182
|
+
await self._handle_message(raw_message)
|
|
183
|
+
except websockets.exceptions.ConnectionClosed:
|
|
184
|
+
if self._stopped:
|
|
185
|
+
break
|
|
186
|
+
reconnect_attempts += 1
|
|
187
|
+
if reconnect_attempts > MAX_RECONNECT_ATTEMPTS:
|
|
188
|
+
logger.error("Max reconnect attempts reached, giving up")
|
|
189
|
+
break
|
|
190
|
+
logger.warning(
|
|
191
|
+
"WebSocket disconnected, reconnecting (attempt %d/%d)",
|
|
192
|
+
reconnect_attempts,
|
|
193
|
+
MAX_RECONNECT_ATTEMPTS,
|
|
194
|
+
)
|
|
195
|
+
try:
|
|
196
|
+
await self._do_connect(self._client_id or str(uuid.uuid4()))
|
|
197
|
+
reconnect_attempts = 0
|
|
198
|
+
except Exception:
|
|
199
|
+
logger.exception("Reconnection failed")
|
|
200
|
+
await asyncio.sleep(1.0)
|
|
201
|
+
except Exception:
|
|
202
|
+
if self._stopped:
|
|
203
|
+
break
|
|
204
|
+
logger.exception("Unexpected error in WebSocket listen loop")
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
async def _handle_message(self, raw_message: str) -> None:
|
|
208
|
+
parsed = json.loads(raw_message)
|
|
209
|
+
msg_type = parsed.get("type")
|
|
210
|
+
msg_value = parsed.get("value")
|
|
211
|
+
|
|
212
|
+
if msg_type == TYPE_CLIENT_ID_DTO:
|
|
213
|
+
value = json.loads(msg_value)
|
|
214
|
+
self._client_id = value["clientId"]
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
if msg_type == TYPE_HTTP_REQUEST:
|
|
218
|
+
request = HttpRequest.from_dict(json.loads(msg_value))
|
|
219
|
+
correlation_id = _extract_correlation_id(request)
|
|
220
|
+
|
|
221
|
+
if self._forward_callback is not None:
|
|
222
|
+
await self._handle_forward_request(request, correlation_id)
|
|
223
|
+
elif self._response_callback is not None:
|
|
224
|
+
await self._handle_response_request(request, correlation_id)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
if msg_type == TYPE_HTTP_REQUEST_AND_RESPONSE:
|
|
228
|
+
req_and_resp = HttpRequestAndHttpResponse.from_dict(json.loads(msg_value))
|
|
229
|
+
correlation_id = _extract_correlation_id(req_and_resp.http_request)
|
|
230
|
+
|
|
231
|
+
if self._forward_response_callback is not None:
|
|
232
|
+
await self._handle_forward_response(req_and_resp, correlation_id)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
logger.warning("Received unhandled WebSocket message type: %s", msg_type)
|
|
236
|
+
|
|
237
|
+
async def _handle_response_request(
|
|
238
|
+
self, request: HttpRequest, correlation_id: str | None
|
|
239
|
+
) -> None:
|
|
240
|
+
try:
|
|
241
|
+
result = self._response_callback(request)
|
|
242
|
+
if asyncio.iscoroutine(result) or asyncio.isfuture(result):
|
|
243
|
+
result = await result
|
|
244
|
+
if not isinstance(result, HttpResponse):
|
|
245
|
+
raise MockServerCallbackError(
|
|
246
|
+
f"Response callback must return HttpResponse, got {type(result)}"
|
|
247
|
+
)
|
|
248
|
+
if correlation_id:
|
|
249
|
+
_add_correlation_id_header(result, correlation_id)
|
|
250
|
+
msg = _build_ws_message(TYPE_HTTP_RESPONSE, result.to_dict())
|
|
251
|
+
await self._ws.send(msg)
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
logger.exception("Error in response callback")
|
|
254
|
+
if correlation_id:
|
|
255
|
+
error_msg = _build_error_message(str(exc), correlation_id)
|
|
256
|
+
await self._ws.send(error_msg)
|
|
257
|
+
|
|
258
|
+
async def _handle_forward_request(
|
|
259
|
+
self, request: HttpRequest, correlation_id: str | None
|
|
260
|
+
) -> None:
|
|
261
|
+
try:
|
|
262
|
+
result = self._forward_callback(request)
|
|
263
|
+
if asyncio.iscoroutine(result) or asyncio.isfuture(result):
|
|
264
|
+
result = await result
|
|
265
|
+
if not isinstance(result, HttpRequest):
|
|
266
|
+
raise MockServerCallbackError(
|
|
267
|
+
f"Forward callback must return HttpRequest, got {type(result)}"
|
|
268
|
+
)
|
|
269
|
+
if correlation_id:
|
|
270
|
+
_add_correlation_id_header(result, correlation_id)
|
|
271
|
+
msg = _build_ws_message(TYPE_HTTP_REQUEST, result.to_dict())
|
|
272
|
+
await self._ws.send(msg)
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
logger.exception("Error in forward callback")
|
|
275
|
+
if correlation_id:
|
|
276
|
+
error_msg = _build_error_message(str(exc), correlation_id)
|
|
277
|
+
await self._ws.send(error_msg)
|
|
278
|
+
|
|
279
|
+
async def _handle_forward_response(
|
|
280
|
+
self,
|
|
281
|
+
req_and_resp: HttpRequestAndHttpResponse,
|
|
282
|
+
correlation_id: str | None,
|
|
283
|
+
) -> None:
|
|
284
|
+
try:
|
|
285
|
+
result = self._forward_response_callback(
|
|
286
|
+
req_and_resp.http_request, req_and_resp.http_response
|
|
287
|
+
)
|
|
288
|
+
if asyncio.iscoroutine(result) or asyncio.isfuture(result):
|
|
289
|
+
result = await result
|
|
290
|
+
if not isinstance(result, HttpResponse):
|
|
291
|
+
raise MockServerCallbackError(
|
|
292
|
+
f"Forward response callback must return HttpResponse, got {type(result)}"
|
|
293
|
+
)
|
|
294
|
+
if correlation_id:
|
|
295
|
+
_add_correlation_id_header(result, correlation_id)
|
|
296
|
+
msg = _build_ws_message(TYPE_HTTP_RESPONSE, result.to_dict())
|
|
297
|
+
await self._ws.send(msg)
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
logger.exception("Error in forward response callback")
|
|
300
|
+
if correlation_id:
|
|
301
|
+
error_msg = _build_error_message(str(exc), correlation_id)
|
|
302
|
+
await self._ws.send(error_msg)
|
|
303
|
+
|
|
304
|
+
async def close(self) -> None:
|
|
305
|
+
self._stopped = True
|
|
306
|
+
if self._listen_task and not self._listen_task.done():
|
|
307
|
+
self._listen_task.cancel()
|
|
308
|
+
try:
|
|
309
|
+
await self._listen_task
|
|
310
|
+
except asyncio.CancelledError:
|
|
311
|
+
pass
|
|
312
|
+
if self._ws and self._ws.open:
|
|
313
|
+
await self._ws.close()
|
|
314
|
+
self._ws = None
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mockserver-client
|
|
3
|
+
Version: 5.16.0
|
|
4
|
+
Summary: Official Python client for MockServer — create expectations, verify requests, and register dynamic callbacks via WebSocket
|
|
5
|
+
Author-email: James Bloom <jamesdbloom@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://www.mock-server.com
|
|
8
|
+
Project-URL: Repository, https://github.com/mock-server/mockserver-monorepo
|
|
9
|
+
Project-URL: Documentation, https://www.mock-server.com/mock_server/getting_started.html
|
|
10
|
+
Project-URL: Changelog, https://www.mock-server.com/mock_server/changelog.html
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/mock-server/mockserver-monorepo/issues
|
|
12
|
+
Keywords: MockServer,HTTP Mock,Proxy,Testing,API,WebSocket,Callback,MCP,AI
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: websockets>=15.0.1
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.4.2; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# MockServer Python Client
|
|
32
|
+
|
|
33
|
+
Python client for [MockServer](https://www.mock-server.com) with full WebSocket callback support.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Full REST API**: Create expectations, verify requests, clear/reset, retrieve recorded data
|
|
38
|
+
- **Response Callbacks**: Register Python functions that dynamically generate responses via WebSocket
|
|
39
|
+
- **Forward Callbacks**: Modify requests before they are forwarded to the real server
|
|
40
|
+
- **Forward+Response Callbacks**: Modify both the forwarded request and the response
|
|
41
|
+
- **Fluent API**: `client.when(request).respond(callback)` — mirrors the Java client
|
|
42
|
+
- **Async + Sync**: Native asyncio API with a synchronous wrapper for non-async code
|
|
43
|
+
- **Minimal Dependencies**: Only `websockets` (for callback support)
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install mockserver-client
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### Synchronous API
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from mockserver import MockServerClient, HttpRequest, HttpResponse
|
|
57
|
+
|
|
58
|
+
client = MockServerClient("localhost", 1080)
|
|
59
|
+
|
|
60
|
+
# Static expectation
|
|
61
|
+
client.when(
|
|
62
|
+
HttpRequest.request("/api/users").with_method("GET")
|
|
63
|
+
).respond(
|
|
64
|
+
HttpResponse.response('{"users": []}', status_code=200)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Verify
|
|
68
|
+
client.verify(
|
|
69
|
+
HttpRequest.request("/api/users").with_method("GET"),
|
|
70
|
+
VerificationTimes.at_least(1)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Clean up
|
|
74
|
+
client.reset()
|
|
75
|
+
client.close()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Context Manager
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
with MockServerClient("localhost", 1080) as client:
|
|
82
|
+
client.when(
|
|
83
|
+
HttpRequest.request("/hello")
|
|
84
|
+
).respond(
|
|
85
|
+
HttpResponse.response("world")
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Async API
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import asyncio
|
|
93
|
+
from mockserver import AsyncMockServerClient, HttpRequest, HttpResponse
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
async with AsyncMockServerClient("localhost", 1080) as client:
|
|
97
|
+
await client.when(
|
|
98
|
+
HttpRequest.request("/api/data")
|
|
99
|
+
).respond(
|
|
100
|
+
HttpResponse.response('{"key": "value"}')
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Response Callbacks
|
|
107
|
+
|
|
108
|
+
Register a Python function that generates responses dynamically when matching requests arrive:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from mockserver import MockServerClient, HttpRequest, HttpResponse
|
|
112
|
+
|
|
113
|
+
def handle_request(request):
|
|
114
|
+
if request.method == "POST":
|
|
115
|
+
return HttpResponse.response("created", status_code=201)
|
|
116
|
+
return HttpResponse.not_found_response()
|
|
117
|
+
|
|
118
|
+
client = MockServerClient("localhost", 1080)
|
|
119
|
+
client.mock_with_callback(
|
|
120
|
+
HttpRequest.request("/api/callback"),
|
|
121
|
+
handle_request
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or with the fluent API:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
client.when(
|
|
129
|
+
HttpRequest.request("/api/callback")
|
|
130
|
+
).respond(handle_request)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Forward Callbacks
|
|
134
|
+
|
|
135
|
+
Modify requests before they are forwarded to the real server:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
def modify_request(request):
|
|
139
|
+
return request.with_header("X-Forwarded", "true").with_path("/modified" + request.path)
|
|
140
|
+
|
|
141
|
+
client.mock_with_forward_callback(
|
|
142
|
+
HttpRequest.request("/proxy/.*"),
|
|
143
|
+
modify_request
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Forward+Response Callbacks
|
|
148
|
+
|
|
149
|
+
Modify both the forwarded request and the response:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
def modify_request(request):
|
|
153
|
+
return request.with_header("X-Proxied", "true")
|
|
154
|
+
|
|
155
|
+
def modify_response(request, response):
|
|
156
|
+
return response.with_header("X-Modified", "true")
|
|
157
|
+
|
|
158
|
+
client.mock_with_forward_callback(
|
|
159
|
+
HttpRequest.request("/proxy/.*"),
|
|
160
|
+
modify_request,
|
|
161
|
+
modify_response
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Verification
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from mockserver import VerificationTimes
|
|
169
|
+
|
|
170
|
+
# Verify a request was received at least once
|
|
171
|
+
client.verify(
|
|
172
|
+
HttpRequest.request("/api/users").with_method("GET"),
|
|
173
|
+
VerificationTimes.at_least(1)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Verify exact count
|
|
177
|
+
client.verify(
|
|
178
|
+
HttpRequest.request("/api/users"),
|
|
179
|
+
VerificationTimes.exactly(3)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Verify request sequence (order matters)
|
|
183
|
+
client.verify_sequence(
|
|
184
|
+
HttpRequest.request("/first"),
|
|
185
|
+
HttpRequest.request("/second"),
|
|
186
|
+
HttpRequest.request("/third"),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Verify no interactions
|
|
190
|
+
client.verify_zero_interactions()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Retrieval
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# Get recorded requests
|
|
197
|
+
requests = client.retrieve_recorded_requests(
|
|
198
|
+
HttpRequest.request("/api/.*")
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Get active expectations
|
|
202
|
+
expectations = client.retrieve_active_expectations()
|
|
203
|
+
|
|
204
|
+
# Get log messages
|
|
205
|
+
logs = client.retrieve_log_messages()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Control
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# Clear specific expectations
|
|
212
|
+
client.clear(HttpRequest.request("/api/users"))
|
|
213
|
+
|
|
214
|
+
# Clear by type
|
|
215
|
+
client.clear(HttpRequest.request("/api/users"), clear_type="LOG")
|
|
216
|
+
|
|
217
|
+
# Reset everything
|
|
218
|
+
client.reset()
|
|
219
|
+
|
|
220
|
+
# Bind additional ports
|
|
221
|
+
client.bind(1081, 1082)
|
|
222
|
+
|
|
223
|
+
# Check if running
|
|
224
|
+
if client.has_started():
|
|
225
|
+
print("MockServer is running")
|
|
226
|
+
|
|
227
|
+
# Stop
|
|
228
|
+
client.stop()
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## TLS Support
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Uses system trust store (default — verifies certificates)
|
|
235
|
+
client = MockServerClient("localhost", 1080, secure=True)
|
|
236
|
+
|
|
237
|
+
# Custom CA certificate
|
|
238
|
+
client = MockServerClient(
|
|
239
|
+
"localhost", 1080,
|
|
240
|
+
secure=True,
|
|
241
|
+
ca_cert_path="/path/to/ca.pem"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Disable certificate verification (testing only — NOT recommended for production)
|
|
245
|
+
client = MockServerClient(
|
|
246
|
+
"localhost", 1080,
|
|
247
|
+
secure=True,
|
|
248
|
+
tls_verify=False
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Domain Model
|
|
253
|
+
|
|
254
|
+
All domain model classes support builder-style chaining:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
request = (
|
|
258
|
+
HttpRequest.request("/api/users")
|
|
259
|
+
.with_method("POST")
|
|
260
|
+
.with_header("Content-Type", "application/json")
|
|
261
|
+
.with_header("Authorization", "Bearer token")
|
|
262
|
+
.with_body('{"name": "test"}')
|
|
263
|
+
.with_query_param("page", "1")
|
|
264
|
+
.with_secure(True)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
response = (
|
|
268
|
+
HttpResponse.response()
|
|
269
|
+
.with_status_code(201)
|
|
270
|
+
.with_header("Location", "/api/users/1")
|
|
271
|
+
.with_body('{"id": 1, "name": "test"}')
|
|
272
|
+
.with_delay(Delay(time_unit="SECONDS", value=1))
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Requirements
|
|
277
|
+
|
|
278
|
+
- Python 3.9+
|
|
279
|
+
- `websockets` >= 12.0 (for callback support)
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
Apache 2.0
|
|
284
|
+
|
|
285
|
+
## AI Assistant Integration
|
|
286
|
+
|
|
287
|
+
MockServer includes a built-in [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server that enables AI coding assistants to create expectations, verify requests, and debug HTTP traffic programmatically.
|
|
288
|
+
|
|
289
|
+
- **MCP Endpoint:** `http://localhost:1080/mockserver/mcp`
|
|
290
|
+
- **AI Documentation:** [llms.txt](https://www.mock-server.com/llms.txt)
|
|
291
|
+
- **Setup Guide:** [AI Integration](https://www.mock-server.com/mock_server/ai_mcp_setup.html)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
mockserver/__init__.py,sha256=RtSodGAfkPBxKhCyWiYRjUi5WVf-XLfIDrP3fpWHyKk,1875
|
|
2
|
+
mockserver/async_client.py,sha256=mabjFlRI8eC59FCCx2c2G9uAFCDsSkbcCTEaDjmzm0s,15005
|
|
3
|
+
mockserver/client.py,sha256=etC8LV7qmFsnmWCkRzQfzj4dYaWVSaySDrdsxIvdFbc,6858
|
|
4
|
+
mockserver/exceptions.py,sha256=aWrjzXgYgAHzLdRDhbcEFu1-Fojcd7bkH6UDy82Hlsw,339
|
|
5
|
+
mockserver/fluent.py,sha256=HFa4mqtw2u_EWooS0xWlk8y9aSXk--U6F7z1UqUHWYA,4534
|
|
6
|
+
mockserver/models.py,sha256=7IKvHzp16BOKQTJnUiJNnzvmLl7OSRB8QvheF0SRR5s,46520
|
|
7
|
+
mockserver/websocket_client.py,sha256=p366vgMmoDfKOGWXOoab81yvXzVYZzS8Q5SmmpqpQSo,11397
|
|
8
|
+
mockserver_client-5.16.0.dist-info/METADATA,sha256=4LLGgu7f5wjwx6YTkQnQkuu-ulN__IrhyQnS-3cStyY,7545
|
|
9
|
+
mockserver_client-5.16.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
mockserver_client-5.16.0.dist-info/top_level.txt,sha256=MvCCh3LtK2uqvpmaxJVmKv3b1tlbpVFOCK8NFeDBJkU,11
|
|
11
|
+
mockserver_client-5.16.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mockserver
|