mockserver-client 7.0.0__tar.gz → 7.1.0__tar.gz
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_client-7.0.0 → mockserver_client-7.1.0}/PKG-INFO +113 -1
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/README.md +112 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/__init__.py +14 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/async_client.py +185 -4
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/client.py +62 -4
- mockserver_client-7.1.0/mockserver/launcher.py +573 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/models.py +28 -3
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/websocket_client.py +150 -2
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/PKG-INFO +113 -1
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/SOURCES.txt +3 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/pyproject.toml +2 -2
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_async_client.py +38 -0
- mockserver_client-7.1.0/tests/test_breakpoint.py +481 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_client.py +35 -0
- mockserver_client-7.1.0/tests/test_launcher.py +817 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_models.py +299 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/exceptions.py +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver/fluent.py +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/dependency_links.txt +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/requires.txt +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/mockserver_client.egg-info/top_level.txt +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/setup.cfg +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_exceptions.py +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_fluent.py +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_init.py +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_integration.py +0 -0
- {mockserver_client-7.0.0 → mockserver_client-7.1.0}/tests/test_websocket_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mockserver-client
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.1.0
|
|
4
4
|
Summary: Official Python client for MockServer — create expectations, verify requests, and register dynamic callbacks via WebSocket
|
|
5
5
|
Author-email: James Bloom <jamesdbloom@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -273,6 +273,118 @@ response = (
|
|
|
273
273
|
)
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
+
## Interactive Breakpoints
|
|
277
|
+
|
|
278
|
+
The client supports matcher-driven interactive breakpoints over the callback WebSocket. Register a breakpoint matcher to pause forwarded/proxied exchanges at specific phases and inspect/modify/continue them via callback handlers.
|
|
279
|
+
|
|
280
|
+
### Register a breakpoint (sync client)
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from mockserver import MockServerClient, HttpRequest, HttpResponse
|
|
284
|
+
|
|
285
|
+
client = MockServerClient("localhost", 1080)
|
|
286
|
+
|
|
287
|
+
# REQUEST phase only
|
|
288
|
+
bp_id = client.add_request_breakpoint(
|
|
289
|
+
HttpRequest(path="/api/.*"),
|
|
290
|
+
lambda request: request, # continue unchanged (or return HttpResponse to abort)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# REQUEST + RESPONSE
|
|
294
|
+
bp_id = client.add_request_and_response_breakpoint(
|
|
295
|
+
HttpRequest(path="/api/.*"),
|
|
296
|
+
lambda request: request, # REQUEST handler
|
|
297
|
+
lambda request, response: response, # RESPONSE handler
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# All phases with stream frame handler
|
|
301
|
+
bp_id = client.add_breakpoint(
|
|
302
|
+
HttpRequest(path="/stream/.*"),
|
|
303
|
+
["REQUEST", "RESPONSE", "RESPONSE_STREAM", "INBOUND_STREAM"],
|
|
304
|
+
request_handler=lambda request: request,
|
|
305
|
+
response_handler=lambda request, response: response,
|
|
306
|
+
stream_frame_handler=lambda frame: {"action": "CONTINUE"},
|
|
307
|
+
# Other actions: MODIFY (with body), DROP, INJECT (with body), CLOSE
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Manage breakpoints
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
# List all matchers
|
|
315
|
+
matchers = client.list_breakpoint_matchers() # {"matchers": [...]}
|
|
316
|
+
|
|
317
|
+
# Remove a specific matcher
|
|
318
|
+
client.remove_breakpoint_matcher(bp_id)
|
|
319
|
+
|
|
320
|
+
# Clear all matchers
|
|
321
|
+
client.clear_breakpoint_matchers()
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The async client (`AsyncMockServerClient`) exposes the same methods as coroutines.
|
|
325
|
+
|
|
326
|
+
## Start / Launch MockServer
|
|
327
|
+
|
|
328
|
+
The Python client can download and launch a local MockServer instance directly -- no Java installation and no Docker required. The launcher downloads a self-contained platform bundle (`mockserver-<version>-<os>-<arch>`) from the GitHub Release, verifies its SHA-256, caches it per-user, and starts it.
|
|
329
|
+
|
|
330
|
+
### Quick start
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
from mockserver.launcher import start, MockServerProcess
|
|
334
|
+
|
|
335
|
+
# Download (first run) and start MockServer on port 1080
|
|
336
|
+
with start(port=1080) as server:
|
|
337
|
+
print(f"MockServer running on port {server.port}, PID {server.pid}")
|
|
338
|
+
# ... use MockServer ...
|
|
339
|
+
# Server is stopped automatically when the context manager exits
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Just ensure the binary is present
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
from mockserver.launcher import ensure_binary
|
|
346
|
+
|
|
347
|
+
launcher_path = ensure_binary() # returns Path to the launcher executable
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Specify a version
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
from mockserver.launcher import start
|
|
354
|
+
|
|
355
|
+
server = start(port=1080, version="7.1.0")
|
|
356
|
+
# ...
|
|
357
|
+
server.stop()
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### API reference
|
|
361
|
+
|
|
362
|
+
| Function / Class | Description |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `ensure_binary(version=None, *, log=True)` | Download, verify, cache, and return the launcher `Path`. Defaults to the client's own version. |
|
|
365
|
+
| `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
|
|
366
|
+
| `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
|
|
367
|
+
|
|
368
|
+
### Supported platforms
|
|
369
|
+
|
|
370
|
+
| OS | Architecture |
|
|
371
|
+
|---|---|
|
|
372
|
+
| Linux | x86_64, aarch64 |
|
|
373
|
+
| macOS (darwin) | x86_64, aarch64 |
|
|
374
|
+
| Windows | x86_64, aarch64 |
|
|
375
|
+
|
|
376
|
+
### Environment variables
|
|
377
|
+
|
|
378
|
+
| Variable | Purpose |
|
|
379
|
+
|---|---|
|
|
380
|
+
| `MOCKSERVER_BINARY_BASE_URL` | Mirror host for the release assets (corporate / air-gapped networks) |
|
|
381
|
+
| `MOCKSERVER_BINARY_CACHE` | Override the cache directory (default: `~/.cache/mockserver/binaries` on Unix) |
|
|
382
|
+
| `MOCKSERVER_SKIP_BINARY_DOWNLOAD` | Fail instead of downloading (use with a pre-seeded cache in CI) |
|
|
383
|
+
|
|
384
|
+
### Version
|
|
385
|
+
|
|
386
|
+
By default the launcher downloads the MockServer version matching this client package (currently the version set in `pyproject.toml`). Pass an explicit `version` argument to override.
|
|
387
|
+
|
|
276
388
|
## Requirements
|
|
277
389
|
|
|
278
390
|
- Python 3.9+
|
|
@@ -243,6 +243,118 @@ response = (
|
|
|
243
243
|
)
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
+
## Interactive Breakpoints
|
|
247
|
+
|
|
248
|
+
The client supports matcher-driven interactive breakpoints over the callback WebSocket. Register a breakpoint matcher to pause forwarded/proxied exchanges at specific phases and inspect/modify/continue them via callback handlers.
|
|
249
|
+
|
|
250
|
+
### Register a breakpoint (sync client)
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from mockserver import MockServerClient, HttpRequest, HttpResponse
|
|
254
|
+
|
|
255
|
+
client = MockServerClient("localhost", 1080)
|
|
256
|
+
|
|
257
|
+
# REQUEST phase only
|
|
258
|
+
bp_id = client.add_request_breakpoint(
|
|
259
|
+
HttpRequest(path="/api/.*"),
|
|
260
|
+
lambda request: request, # continue unchanged (or return HttpResponse to abort)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# REQUEST + RESPONSE
|
|
264
|
+
bp_id = client.add_request_and_response_breakpoint(
|
|
265
|
+
HttpRequest(path="/api/.*"),
|
|
266
|
+
lambda request: request, # REQUEST handler
|
|
267
|
+
lambda request, response: response, # RESPONSE handler
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# All phases with stream frame handler
|
|
271
|
+
bp_id = client.add_breakpoint(
|
|
272
|
+
HttpRequest(path="/stream/.*"),
|
|
273
|
+
["REQUEST", "RESPONSE", "RESPONSE_STREAM", "INBOUND_STREAM"],
|
|
274
|
+
request_handler=lambda request: request,
|
|
275
|
+
response_handler=lambda request, response: response,
|
|
276
|
+
stream_frame_handler=lambda frame: {"action": "CONTINUE"},
|
|
277
|
+
# Other actions: MODIFY (with body), DROP, INJECT (with body), CLOSE
|
|
278
|
+
)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Manage breakpoints
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
# List all matchers
|
|
285
|
+
matchers = client.list_breakpoint_matchers() # {"matchers": [...]}
|
|
286
|
+
|
|
287
|
+
# Remove a specific matcher
|
|
288
|
+
client.remove_breakpoint_matcher(bp_id)
|
|
289
|
+
|
|
290
|
+
# Clear all matchers
|
|
291
|
+
client.clear_breakpoint_matchers()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The async client (`AsyncMockServerClient`) exposes the same methods as coroutines.
|
|
295
|
+
|
|
296
|
+
## Start / Launch MockServer
|
|
297
|
+
|
|
298
|
+
The Python client can download and launch a local MockServer instance directly -- no Java installation and no Docker required. The launcher downloads a self-contained platform bundle (`mockserver-<version>-<os>-<arch>`) from the GitHub Release, verifies its SHA-256, caches it per-user, and starts it.
|
|
299
|
+
|
|
300
|
+
### Quick start
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from mockserver.launcher import start, MockServerProcess
|
|
304
|
+
|
|
305
|
+
# Download (first run) and start MockServer on port 1080
|
|
306
|
+
with start(port=1080) as server:
|
|
307
|
+
print(f"MockServer running on port {server.port}, PID {server.pid}")
|
|
308
|
+
# ... use MockServer ...
|
|
309
|
+
# Server is stopped automatically when the context manager exits
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Just ensure the binary is present
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from mockserver.launcher import ensure_binary
|
|
316
|
+
|
|
317
|
+
launcher_path = ensure_binary() # returns Path to the launcher executable
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Specify a version
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from mockserver.launcher import start
|
|
324
|
+
|
|
325
|
+
server = start(port=1080, version="7.1.0")
|
|
326
|
+
# ...
|
|
327
|
+
server.stop()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### API reference
|
|
331
|
+
|
|
332
|
+
| Function / Class | Description |
|
|
333
|
+
|---|---|
|
|
334
|
+
| `ensure_binary(version=None, *, log=True)` | Download, verify, cache, and return the launcher `Path`. Defaults to the client's own version. |
|
|
335
|
+
| `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
|
|
336
|
+
| `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
|
|
337
|
+
|
|
338
|
+
### Supported platforms
|
|
339
|
+
|
|
340
|
+
| OS | Architecture |
|
|
341
|
+
|---|---|
|
|
342
|
+
| Linux | x86_64, aarch64 |
|
|
343
|
+
| macOS (darwin) | x86_64, aarch64 |
|
|
344
|
+
| Windows | x86_64, aarch64 |
|
|
345
|
+
|
|
346
|
+
### Environment variables
|
|
347
|
+
|
|
348
|
+
| Variable | Purpose |
|
|
349
|
+
|---|---|
|
|
350
|
+
| `MOCKSERVER_BINARY_BASE_URL` | Mirror host for the release assets (corporate / air-gapped networks) |
|
|
351
|
+
| `MOCKSERVER_BINARY_CACHE` | Override the cache directory (default: `~/.cache/mockserver/binaries` on Unix) |
|
|
352
|
+
| `MOCKSERVER_SKIP_BINARY_DOWNLOAD` | Fail instead of downloading (use with a pre-seeded cache in CI) |
|
|
353
|
+
|
|
354
|
+
### Version
|
|
355
|
+
|
|
356
|
+
By default the launcher downloads the MockServer version matching this client package (currently the version set in `pyproject.toml`). Pass an explicit `version` argument to override.
|
|
357
|
+
|
|
246
358
|
## Requirements
|
|
247
359
|
|
|
248
360
|
- Python 3.9+
|
|
@@ -50,6 +50,14 @@ from mockserver.exceptions import (
|
|
|
50
50
|
)
|
|
51
51
|
from mockserver.client import MockServerClient
|
|
52
52
|
from mockserver.async_client import AsyncMockServerClient
|
|
53
|
+
from mockserver.launcher import (
|
|
54
|
+
MockServerProcess,
|
|
55
|
+
bundle_base_name,
|
|
56
|
+
cache_dir,
|
|
57
|
+
ensure_binary,
|
|
58
|
+
resolve_platform,
|
|
59
|
+
start as start_server,
|
|
60
|
+
)
|
|
53
61
|
|
|
54
62
|
__all__ = [
|
|
55
63
|
"AfterAction",
|
|
@@ -100,4 +108,10 @@ __all__ = [
|
|
|
100
108
|
"VerificationSequence",
|
|
101
109
|
"VerificationTimes",
|
|
102
110
|
"WebSocketMessage",
|
|
111
|
+
"MockServerProcess",
|
|
112
|
+
"bundle_base_name",
|
|
113
|
+
"cache_dir",
|
|
114
|
+
"ensure_binary",
|
|
115
|
+
"resolve_platform",
|
|
116
|
+
"start_server",
|
|
103
117
|
]
|
|
@@ -284,10 +284,12 @@ class AsyncMockServerClient:
|
|
|
284
284
|
|
|
285
285
|
async def verify(
|
|
286
286
|
self,
|
|
287
|
-
request: HttpRequest,
|
|
287
|
+
request: HttpRequest | None = None,
|
|
288
288
|
times: VerificationTimes | None = None,
|
|
289
|
+
*,
|
|
290
|
+
response: HttpResponse | None = None,
|
|
289
291
|
) -> None:
|
|
290
|
-
verification = Verification(http_request=request, times=times)
|
|
292
|
+
verification = Verification(http_request=request, http_response=response, times=times)
|
|
291
293
|
body = json.dumps(verification.to_dict())
|
|
292
294
|
status, response_body = await self._request("PUT", "/mockserver/verify", body)
|
|
293
295
|
if status == 406:
|
|
@@ -297,9 +299,14 @@ class AsyncMockServerClient:
|
|
|
297
299
|
f"Failed to verify (status={status}): {response_body}"
|
|
298
300
|
)
|
|
299
301
|
|
|
300
|
-
async def verify_sequence(
|
|
302
|
+
async def verify_sequence(
|
|
303
|
+
self,
|
|
304
|
+
*requests: HttpRequest,
|
|
305
|
+
responses: list[HttpResponse] | None = None,
|
|
306
|
+
) -> None:
|
|
301
307
|
verification = VerificationSequence(
|
|
302
|
-
http_requests=list(requests)
|
|
308
|
+
http_requests=list(requests) if requests else None,
|
|
309
|
+
http_responses=responses,
|
|
303
310
|
)
|
|
304
311
|
body = json.dumps(verification.to_dict())
|
|
305
312
|
status, response_body = await self._request(
|
|
@@ -504,6 +511,180 @@ class AsyncMockServerClient:
|
|
|
504
511
|
)
|
|
505
512
|
return await self.upsert(expectation)
|
|
506
513
|
|
|
514
|
+
# -------------------------------------------------------------------
|
|
515
|
+
# Breakpoint matcher management
|
|
516
|
+
# -------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
async def _ensure_breakpoint_websocket(self) -> MockServerWebSocketClient:
|
|
519
|
+
"""Ensure a callback WS is connected for breakpoint use, returning it."""
|
|
520
|
+
# Serialise lazy creation so concurrent add_breakpoint coroutines
|
|
521
|
+
# (e.g. via asyncio.gather) cannot each create a duplicate breakpoint WS
|
|
522
|
+
# across the await in connect(). Creating/assigning the lock has no await
|
|
523
|
+
# between the check and the set, so it is atomic on the event loop.
|
|
524
|
+
lock = getattr(self, '_breakpoint_ws_lock', None)
|
|
525
|
+
if lock is None:
|
|
526
|
+
lock = asyncio.Lock()
|
|
527
|
+
self._breakpoint_ws_lock = lock # type: ignore[attr-defined]
|
|
528
|
+
async with lock:
|
|
529
|
+
# Reuse the first WS client tagged as breakpoint, or create one
|
|
530
|
+
for ws in self._websocket_clients:
|
|
531
|
+
if getattr(ws, '_is_breakpoint_ws', False):
|
|
532
|
+
return ws
|
|
533
|
+
|
|
534
|
+
ws_client = MockServerWebSocketClient()
|
|
535
|
+
await ws_client.connect(
|
|
536
|
+
self._host,
|
|
537
|
+
self._port,
|
|
538
|
+
self._context_path,
|
|
539
|
+
self._secure,
|
|
540
|
+
self._ca_cert_path,
|
|
541
|
+
tls_verify=self._tls_verify,
|
|
542
|
+
)
|
|
543
|
+
ws_client._is_breakpoint_ws = True # type: ignore[attr-defined]
|
|
544
|
+
ws_client._listen_task = asyncio.create_task(ws_client.listen())
|
|
545
|
+
self._websocket_clients.append(ws_client)
|
|
546
|
+
return ws_client
|
|
547
|
+
|
|
548
|
+
async def add_breakpoint(
|
|
549
|
+
self,
|
|
550
|
+
matcher: HttpRequest,
|
|
551
|
+
phases: list[str],
|
|
552
|
+
request_handler: Callable | None = None,
|
|
553
|
+
response_handler: Callable | None = None,
|
|
554
|
+
stream_frame_handler: Callable | None = None,
|
|
555
|
+
) -> str:
|
|
556
|
+
"""Register a breakpoint matcher with callback handlers.
|
|
557
|
+
|
|
558
|
+
The callback WebSocket is opened lazily on the first call and reused.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
matcher: the request definition to match (same as expectation matcher)
|
|
562
|
+
phases: list of phase strings: "REQUEST", "RESPONSE",
|
|
563
|
+
"RESPONSE_STREAM", "INBOUND_STREAM"
|
|
564
|
+
request_handler: callable(HttpRequest) -> HttpRequest|HttpResponse
|
|
565
|
+
for REQUEST phase (optional)
|
|
566
|
+
response_handler: callable(HttpRequest, HttpResponse) -> HttpResponse
|
|
567
|
+
for RESPONSE phase (optional)
|
|
568
|
+
stream_frame_handler: callable(dict) -> dict for streaming phases
|
|
569
|
+
(optional). Receives PausedStreamFrameDTO dict,
|
|
570
|
+
returns StreamFrameDecisionDTO dict.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
The server-assigned breakpoint matcher id (UUID string).
|
|
574
|
+
"""
|
|
575
|
+
if matcher is None:
|
|
576
|
+
raise ValueError("add_breakpoint requires a non-None matcher")
|
|
577
|
+
if not phases:
|
|
578
|
+
raise ValueError("add_breakpoint requires a non-empty phases list")
|
|
579
|
+
|
|
580
|
+
ws_client = await self._ensure_breakpoint_websocket()
|
|
581
|
+
client_id = ws_client.client_id
|
|
582
|
+
|
|
583
|
+
body = json.dumps({
|
|
584
|
+
"httpRequest": matcher.to_dict(),
|
|
585
|
+
"phases": phases,
|
|
586
|
+
"clientId": client_id,
|
|
587
|
+
})
|
|
588
|
+
status, response_body = await self._request(
|
|
589
|
+
"PUT", "/mockserver/breakpoint/matcher", body
|
|
590
|
+
)
|
|
591
|
+
if status >= 400:
|
|
592
|
+
raise MockServerError(
|
|
593
|
+
f"Failed to register breakpoint matcher (status={status}): {response_body}"
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
parsed = json.loads(response_body) if response_body else {}
|
|
597
|
+
breakpoint_id = parsed.get("id")
|
|
598
|
+
if not breakpoint_id:
|
|
599
|
+
raise MockServerError("Server did not return a breakpoint id")
|
|
600
|
+
|
|
601
|
+
# Install per-breakpoint-id handlers
|
|
602
|
+
if request_handler:
|
|
603
|
+
ws_client.set_breakpoint_request_handler(breakpoint_id, request_handler)
|
|
604
|
+
if response_handler:
|
|
605
|
+
ws_client.set_breakpoint_response_handler(breakpoint_id, response_handler)
|
|
606
|
+
if stream_frame_handler:
|
|
607
|
+
ws_client.set_breakpoint_stream_frame_handler(breakpoint_id, stream_frame_handler)
|
|
608
|
+
|
|
609
|
+
return breakpoint_id
|
|
610
|
+
|
|
611
|
+
async def add_request_breakpoint(
|
|
612
|
+
self,
|
|
613
|
+
matcher: HttpRequest,
|
|
614
|
+
request_handler: Callable,
|
|
615
|
+
) -> str:
|
|
616
|
+
"""Convenience: register a REQUEST-only breakpoint."""
|
|
617
|
+
return await self.add_breakpoint(
|
|
618
|
+
matcher, ["REQUEST"], request_handler=request_handler
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
async def add_request_and_response_breakpoint(
|
|
622
|
+
self,
|
|
623
|
+
matcher: HttpRequest,
|
|
624
|
+
request_handler: Callable,
|
|
625
|
+
response_handler: Callable,
|
|
626
|
+
) -> str:
|
|
627
|
+
"""Convenience: register a REQUEST+RESPONSE breakpoint."""
|
|
628
|
+
return await self.add_breakpoint(
|
|
629
|
+
matcher,
|
|
630
|
+
["REQUEST", "RESPONSE"],
|
|
631
|
+
request_handler=request_handler,
|
|
632
|
+
response_handler=response_handler,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
async def list_breakpoint_matchers(self) -> dict:
|
|
636
|
+
"""List all registered breakpoint matchers.
|
|
637
|
+
|
|
638
|
+
Returns a dict: {"matchers": [{id, httpRequest, phases, clientId}, ...]}.
|
|
639
|
+
"""
|
|
640
|
+
status, response_body = await self._request(
|
|
641
|
+
"GET", "/mockserver/breakpoint/matchers"
|
|
642
|
+
)
|
|
643
|
+
if status >= 400:
|
|
644
|
+
raise MockServerError(
|
|
645
|
+
f"Failed to list breakpoint matchers (status={status}): {response_body}"
|
|
646
|
+
)
|
|
647
|
+
return json.loads(response_body) if response_body else {}
|
|
648
|
+
|
|
649
|
+
async def remove_breakpoint_matcher(self, breakpoint_id: str) -> dict:
|
|
650
|
+
"""Remove a breakpoint matcher by id.
|
|
651
|
+
|
|
652
|
+
Returns a dict: {"status": "removed", "id": "..."} or raises on 404.
|
|
653
|
+
"""
|
|
654
|
+
if not breakpoint_id:
|
|
655
|
+
raise ValueError("remove_breakpoint_matcher requires a non-empty id")
|
|
656
|
+
body = json.dumps({"id": breakpoint_id})
|
|
657
|
+
status, response_body = await self._request(
|
|
658
|
+
"PUT", "/mockserver/breakpoint/matcher/remove", body
|
|
659
|
+
)
|
|
660
|
+
if status >= 400:
|
|
661
|
+
raise MockServerError(
|
|
662
|
+
f"Failed to remove breakpoint matcher (status={status}): {response_body}"
|
|
663
|
+
)
|
|
664
|
+
# Remove client-side handlers
|
|
665
|
+
for ws in self._websocket_clients:
|
|
666
|
+
if getattr(ws, '_is_breakpoint_ws', False):
|
|
667
|
+
ws.remove_breakpoint_handlers(breakpoint_id)
|
|
668
|
+
return json.loads(response_body) if response_body else {}
|
|
669
|
+
|
|
670
|
+
async def clear_breakpoint_matchers(self) -> dict:
|
|
671
|
+
"""Clear all registered breakpoint matchers.
|
|
672
|
+
|
|
673
|
+
Returns a dict: {"status": "cleared", "count": N}.
|
|
674
|
+
"""
|
|
675
|
+
status, response_body = await self._request(
|
|
676
|
+
"PUT", "/mockserver/breakpoint/matcher/clear"
|
|
677
|
+
)
|
|
678
|
+
if status >= 400:
|
|
679
|
+
raise MockServerError(
|
|
680
|
+
f"Failed to clear breakpoint matchers (status={status}): {response_body}"
|
|
681
|
+
)
|
|
682
|
+
# Clear client-side handlers
|
|
683
|
+
for ws in self._websocket_clients:
|
|
684
|
+
if getattr(ws, '_is_breakpoint_ws', False):
|
|
685
|
+
ws.clear_breakpoint_handlers()
|
|
686
|
+
return json.loads(response_body) if response_body else {}
|
|
687
|
+
|
|
507
688
|
async def _register_websocket_callback(
|
|
508
689
|
self,
|
|
509
690
|
callback_type: str,
|
|
@@ -169,13 +169,19 @@ class MockServerClient:
|
|
|
169
169
|
|
|
170
170
|
def verify(
|
|
171
171
|
self,
|
|
172
|
-
request: HttpRequest,
|
|
172
|
+
request: HttpRequest | None = None,
|
|
173
173
|
times: VerificationTimes | None = None,
|
|
174
|
+
*,
|
|
175
|
+
response: HttpResponse | None = None,
|
|
174
176
|
) -> None:
|
|
175
|
-
return self._run(self._async_client.verify(request, times))
|
|
177
|
+
return self._run(self._async_client.verify(request, times, response=response))
|
|
176
178
|
|
|
177
|
-
def verify_sequence(
|
|
178
|
-
|
|
179
|
+
def verify_sequence(
|
|
180
|
+
self,
|
|
181
|
+
*requests: HttpRequest,
|
|
182
|
+
responses: list[HttpResponse] | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
return self._run(self._async_client.verify_sequence(*requests, responses=responses))
|
|
179
185
|
|
|
180
186
|
def verify_zero_interactions(self) -> None:
|
|
181
187
|
return self._run(self._async_client.verify_zero_interactions())
|
|
@@ -237,6 +243,58 @@ class MockServerClient:
|
|
|
237
243
|
self._async_client.mock_with_callback(request, callback, times, time_to_live)
|
|
238
244
|
)
|
|
239
245
|
|
|
246
|
+
# -------------------------------------------------------------------
|
|
247
|
+
# Breakpoint matcher management
|
|
248
|
+
# -------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def add_breakpoint(
|
|
251
|
+
self,
|
|
252
|
+
matcher,
|
|
253
|
+
phases: list[str],
|
|
254
|
+
request_handler=None,
|
|
255
|
+
response_handler=None,
|
|
256
|
+
stream_frame_handler=None,
|
|
257
|
+
) -> str:
|
|
258
|
+
"""Register a breakpoint matcher with callback handlers.
|
|
259
|
+
|
|
260
|
+
Returns the server-assigned breakpoint matcher id (UUID string).
|
|
261
|
+
"""
|
|
262
|
+
return self._run(
|
|
263
|
+
self._async_client.add_breakpoint(
|
|
264
|
+
matcher, phases, request_handler, response_handler, stream_frame_handler
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def add_request_breakpoint(self, matcher, request_handler) -> str:
|
|
269
|
+
"""Convenience: register a REQUEST-only breakpoint."""
|
|
270
|
+
return self._run(
|
|
271
|
+
self._async_client.add_request_breakpoint(matcher, request_handler)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def add_request_and_response_breakpoint(
|
|
275
|
+
self, matcher, request_handler, response_handler
|
|
276
|
+
) -> str:
|
|
277
|
+
"""Convenience: register a REQUEST+RESPONSE breakpoint."""
|
|
278
|
+
return self._run(
|
|
279
|
+
self._async_client.add_request_and_response_breakpoint(
|
|
280
|
+
matcher, request_handler, response_handler
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def list_breakpoint_matchers(self) -> dict:
|
|
285
|
+
"""List all registered breakpoint matchers."""
|
|
286
|
+
return self._run(self._async_client.list_breakpoint_matchers())
|
|
287
|
+
|
|
288
|
+
def remove_breakpoint_matcher(self, breakpoint_id: str) -> dict:
|
|
289
|
+
"""Remove a breakpoint matcher by id."""
|
|
290
|
+
return self._run(
|
|
291
|
+
self._async_client.remove_breakpoint_matcher(breakpoint_id)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def clear_breakpoint_matchers(self) -> dict:
|
|
295
|
+
"""Clear all registered breakpoint matchers."""
|
|
296
|
+
return self._run(self._async_client.clear_breakpoint_matchers())
|
|
297
|
+
|
|
240
298
|
def mock_with_forward_callback(
|
|
241
299
|
self,
|
|
242
300
|
request: HttpRequest,
|