mockserver-client 7.6.0__tar.gz → 9.0.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.6.0 → mockserver_client-9.0.0}/PKG-INFO +86 -2
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/README.md +85 -1
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/__init__.py +32 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/a2a.py +1 -1
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/async_client.py +194 -11
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/client.py +80 -6
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/launcher.py +67 -13
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/models.py +800 -43
- mockserver_client-9.0.0/mockserver/py.typed +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/PKG-INFO +86 -2
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/SOURCES.txt +7 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/pyproject.toml +7 -1
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_a2a.py +3 -3
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_async_client.py +42 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_client.py +73 -2
- mockserver_client-9.0.0/tests/test_conditional_request_and_load_check.py +281 -0
- mockserver_client-9.0.0/tests/test_explain.py +321 -0
- mockserver_client-9.0.0/tests/test_from_dict_fidelity.py +28 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_init.py +2 -0
- mockserver_client-9.0.0/tests/test_key_multi_value_forms.py +186 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_launcher.py +85 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_models.py +115 -3
- mockserver_client-9.0.0/tests/test_override_forwarded_request.py +193 -0
- mockserver_client-9.0.0/tests/test_sync_chain_parity.py +83 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/exceptions.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/fluent.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/llm.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/mcp.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/pytest_plugin.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver/websocket_client.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/dependency_links.txt +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/entry_points.txt +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/requires.txt +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/mockserver_client.egg-info/top_level.txt +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/setup.cfg +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_auth_tls.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_breakpoint.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_control_plane.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_exceptions.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_fluent.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_integration.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_llm.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_mcp.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_pytest_plugin.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.0}/tests/test_roundtrip_fidelity.py +0 -0
- {mockserver_client-7.6.0 → mockserver_client-9.0.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:
|
|
3
|
+
Version: 9.0.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
|
|
@@ -162,6 +162,27 @@ client.mock_with_forward_callback(
|
|
|
162
162
|
)
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
## Forward with Overrides
|
|
166
|
+
|
|
167
|
+
Forward the matched request with fields replaced (`request_override`), modified
|
|
168
|
+
(`request_modifier`), or with its response overridden (`response_override`,
|
|
169
|
+
`response_modifier`, `response_template`), without a callback:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from mockserver import HttpOverrideForwardedRequest, HttpRequest, HttpResponse
|
|
173
|
+
|
|
174
|
+
client.when(HttpRequest.request("/some/path")).forward(
|
|
175
|
+
HttpOverrideForwardedRequest(
|
|
176
|
+
request_override=HttpRequest(path="/other/path", headers={"Host": ["target.host.com"]}),
|
|
177
|
+
response_override=HttpResponse(headers={"X-Proxied": ["true"]}),
|
|
178
|
+
response_modifier={"headers": {"remove": ["Server"]}},
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`http_request` and `http_response` are deprecated aliases of `request_override`
|
|
184
|
+
and `response_override`, kept for existing code; set one name of each pair, not both.
|
|
185
|
+
|
|
165
186
|
## Verification
|
|
166
187
|
|
|
167
188
|
```python
|
|
@@ -192,6 +213,31 @@ client.verify_zero_interactions()
|
|
|
192
213
|
|
|
193
214
|
## Request Matching
|
|
194
215
|
|
|
216
|
+
### Headers, query parameters and cookies
|
|
217
|
+
|
|
218
|
+
Pass headers, query and path parameters, trailers and cookies as a dict, as in
|
|
219
|
+
MockServer's JSON, or as a list of `KeyToMultiValue`. A `"keyMatchStyle"` key
|
|
220
|
+
(`SUB_SET`, the default, or `MATCHING_KEY`) sets how the values of a key are
|
|
221
|
+
matched.
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from mockserver import HttpRequest, HttpResponse
|
|
225
|
+
|
|
226
|
+
client.when(
|
|
227
|
+
HttpRequest(
|
|
228
|
+
path="/search",
|
|
229
|
+
headers={"Accept": "application/json"}, # one value
|
|
230
|
+
query_string_parameters={"tag": ["a", "b"]}, # several values
|
|
231
|
+
cookies={"session": "abc"},
|
|
232
|
+
)
|
|
233
|
+
).respond(
|
|
234
|
+
HttpResponse(status_code=302, headers={"Location": ["https://example.com"]})
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# every value the request has for X-Tag must match, not just one
|
|
238
|
+
HttpRequest(headers={"keyMatchStyle": "MATCHING_KEY", "X-Tag": ["a", "b"]})
|
|
239
|
+
```
|
|
240
|
+
|
|
195
241
|
### JWT matcher
|
|
196
242
|
|
|
197
243
|
Match requests by the claims (and metadata) of a JWT carried in a header. Each
|
|
@@ -247,6 +293,27 @@ HttpRequest.request("/api/orders").with_body(
|
|
|
247
293
|
)
|
|
248
294
|
```
|
|
249
295
|
|
|
296
|
+
### Conditional (if/then/else) matcher
|
|
297
|
+
|
|
298
|
+
Pass a `ConditionalRequestDefinition` where a request matcher goes: when
|
|
299
|
+
`if_request` matches, `then_request` must match too; otherwise `else_request`
|
|
300
|
+
must (with no `else_request` it matches whenever `if_request` does not). Each
|
|
301
|
+
branch is an `HttpRequest`, an `OpenAPIDefinition` or another
|
|
302
|
+
`ConditionalRequestDefinition`. Retrieved expectations whose `httpRequest` holds
|
|
303
|
+
`if` read back as this type.
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from mockserver import Body, ConditionalRequestDefinition, HttpRequest, HttpResponse
|
|
307
|
+
|
|
308
|
+
client.when(
|
|
309
|
+
ConditionalRequestDefinition(
|
|
310
|
+
if_request=HttpRequest(method="POST").with_header("content-type", "application/json"),
|
|
311
|
+
then_request=HttpRequest(body=Body.json_schema('{"type": "object", "required": ["orderId"]}')),
|
|
312
|
+
else_request=HttpRequest(method="GET"),
|
|
313
|
+
)
|
|
314
|
+
).respond(HttpResponse(status_code=200))
|
|
315
|
+
```
|
|
316
|
+
|
|
250
317
|
## Retrieval
|
|
251
318
|
|
|
252
319
|
```python
|
|
@@ -362,6 +429,21 @@ client.start_chaos_experiment({
|
|
|
362
429
|
Both methods are available on the async client too (`await client.verify_slo(...)`,
|
|
363
430
|
`await client.start_chaos_experiment(...)`).
|
|
364
431
|
|
|
432
|
+
## Diagnose Matching
|
|
433
|
+
|
|
434
|
+
`explain(correlation_id)` tells you which expectation would serve a recorded request now and why
|
|
435
|
+
each expectation ahead of it was passed over; `debug_mismatch(request)` compares a request with
|
|
436
|
+
every active expectation; `explain_unmatched(limit=..., suggest_fixes=...)` explains recent
|
|
437
|
+
requests that matched nothing. Each returns a typed dataclass (`RequestExplanation`,
|
|
438
|
+
`DebugMismatchResult`, `ExplainUnmatchedResult`) and raises `MockServerError` with the server's
|
|
439
|
+
reason on failure. The async client has the same methods.
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
explanation = client.explain("abc-123-def-456")
|
|
443
|
+
for skipped in explanation.skipped_expectations:
|
|
444
|
+
print(skipped.expectation_id, skipped.reason, skipped.explanation)
|
|
445
|
+
```
|
|
446
|
+
|
|
365
447
|
## TLS Support
|
|
366
448
|
|
|
367
449
|
```python
|
|
@@ -486,7 +568,7 @@ launcher_path = ensure_binary() # returns Path to the launcher executable
|
|
|
486
568
|
```python
|
|
487
569
|
from mockserver.launcher import start
|
|
488
570
|
|
|
489
|
-
server = start(port=1080, version="
|
|
571
|
+
server = start(port=1080, version="9.0.0")
|
|
490
572
|
# ...
|
|
491
573
|
server.stop()
|
|
492
574
|
```
|
|
@@ -499,6 +581,8 @@ server.stop()
|
|
|
499
581
|
| `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
|
|
500
582
|
| `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
|
|
501
583
|
|
|
584
|
+
On Windows the launcher runs `mockserver.bat` through `cmd.exe` with every argument quoted, so characters such as `&`, `|` and `^` reach MockServer unchanged. `start()` raises `ValueError` if the launcher path or an `extra_args` entry contains a double quote, a line break or NUL, or if together they contain more than one `%` (which `cmd.exe` would expand as a `%NAME%` variable). No shell is used on Linux or macOS.
|
|
585
|
+
|
|
502
586
|
### Supported platforms
|
|
503
587
|
|
|
504
588
|
| OS | Architecture |
|
|
@@ -132,6 +132,27 @@ client.mock_with_forward_callback(
|
|
|
132
132
|
)
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
## Forward with Overrides
|
|
136
|
+
|
|
137
|
+
Forward the matched request with fields replaced (`request_override`), modified
|
|
138
|
+
(`request_modifier`), or with its response overridden (`response_override`,
|
|
139
|
+
`response_modifier`, `response_template`), without a callback:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from mockserver import HttpOverrideForwardedRequest, HttpRequest, HttpResponse
|
|
143
|
+
|
|
144
|
+
client.when(HttpRequest.request("/some/path")).forward(
|
|
145
|
+
HttpOverrideForwardedRequest(
|
|
146
|
+
request_override=HttpRequest(path="/other/path", headers={"Host": ["target.host.com"]}),
|
|
147
|
+
response_override=HttpResponse(headers={"X-Proxied": ["true"]}),
|
|
148
|
+
response_modifier={"headers": {"remove": ["Server"]}},
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`http_request` and `http_response` are deprecated aliases of `request_override`
|
|
154
|
+
and `response_override`, kept for existing code; set one name of each pair, not both.
|
|
155
|
+
|
|
135
156
|
## Verification
|
|
136
157
|
|
|
137
158
|
```python
|
|
@@ -162,6 +183,31 @@ client.verify_zero_interactions()
|
|
|
162
183
|
|
|
163
184
|
## Request Matching
|
|
164
185
|
|
|
186
|
+
### Headers, query parameters and cookies
|
|
187
|
+
|
|
188
|
+
Pass headers, query and path parameters, trailers and cookies as a dict, as in
|
|
189
|
+
MockServer's JSON, or as a list of `KeyToMultiValue`. A `"keyMatchStyle"` key
|
|
190
|
+
(`SUB_SET`, the default, or `MATCHING_KEY`) sets how the values of a key are
|
|
191
|
+
matched.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from mockserver import HttpRequest, HttpResponse
|
|
195
|
+
|
|
196
|
+
client.when(
|
|
197
|
+
HttpRequest(
|
|
198
|
+
path="/search",
|
|
199
|
+
headers={"Accept": "application/json"}, # one value
|
|
200
|
+
query_string_parameters={"tag": ["a", "b"]}, # several values
|
|
201
|
+
cookies={"session": "abc"},
|
|
202
|
+
)
|
|
203
|
+
).respond(
|
|
204
|
+
HttpResponse(status_code=302, headers={"Location": ["https://example.com"]})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# every value the request has for X-Tag must match, not just one
|
|
208
|
+
HttpRequest(headers={"keyMatchStyle": "MATCHING_KEY", "X-Tag": ["a", "b"]})
|
|
209
|
+
```
|
|
210
|
+
|
|
165
211
|
### JWT matcher
|
|
166
212
|
|
|
167
213
|
Match requests by the claims (and metadata) of a JWT carried in a header. Each
|
|
@@ -217,6 +263,27 @@ HttpRequest.request("/api/orders").with_body(
|
|
|
217
263
|
)
|
|
218
264
|
```
|
|
219
265
|
|
|
266
|
+
### Conditional (if/then/else) matcher
|
|
267
|
+
|
|
268
|
+
Pass a `ConditionalRequestDefinition` where a request matcher goes: when
|
|
269
|
+
`if_request` matches, `then_request` must match too; otherwise `else_request`
|
|
270
|
+
must (with no `else_request` it matches whenever `if_request` does not). Each
|
|
271
|
+
branch is an `HttpRequest`, an `OpenAPIDefinition` or another
|
|
272
|
+
`ConditionalRequestDefinition`. Retrieved expectations whose `httpRequest` holds
|
|
273
|
+
`if` read back as this type.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from mockserver import Body, ConditionalRequestDefinition, HttpRequest, HttpResponse
|
|
277
|
+
|
|
278
|
+
client.when(
|
|
279
|
+
ConditionalRequestDefinition(
|
|
280
|
+
if_request=HttpRequest(method="POST").with_header("content-type", "application/json"),
|
|
281
|
+
then_request=HttpRequest(body=Body.json_schema('{"type": "object", "required": ["orderId"]}')),
|
|
282
|
+
else_request=HttpRequest(method="GET"),
|
|
283
|
+
)
|
|
284
|
+
).respond(HttpResponse(status_code=200))
|
|
285
|
+
```
|
|
286
|
+
|
|
220
287
|
## Retrieval
|
|
221
288
|
|
|
222
289
|
```python
|
|
@@ -332,6 +399,21 @@ client.start_chaos_experiment({
|
|
|
332
399
|
Both methods are available on the async client too (`await client.verify_slo(...)`,
|
|
333
400
|
`await client.start_chaos_experiment(...)`).
|
|
334
401
|
|
|
402
|
+
## Diagnose Matching
|
|
403
|
+
|
|
404
|
+
`explain(correlation_id)` tells you which expectation would serve a recorded request now and why
|
|
405
|
+
each expectation ahead of it was passed over; `debug_mismatch(request)` compares a request with
|
|
406
|
+
every active expectation; `explain_unmatched(limit=..., suggest_fixes=...)` explains recent
|
|
407
|
+
requests that matched nothing. Each returns a typed dataclass (`RequestExplanation`,
|
|
408
|
+
`DebugMismatchResult`, `ExplainUnmatchedResult`) and raises `MockServerError` with the server's
|
|
409
|
+
reason on failure. The async client has the same methods.
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
explanation = client.explain("abc-123-def-456")
|
|
413
|
+
for skipped in explanation.skipped_expectations:
|
|
414
|
+
print(skipped.expectation_id, skipped.reason, skipped.explanation)
|
|
415
|
+
```
|
|
416
|
+
|
|
335
417
|
## TLS Support
|
|
336
418
|
|
|
337
419
|
```python
|
|
@@ -456,7 +538,7 @@ launcher_path = ensure_binary() # returns Path to the launcher executable
|
|
|
456
538
|
```python
|
|
457
539
|
from mockserver.launcher import start
|
|
458
540
|
|
|
459
|
-
server = start(port=1080, version="
|
|
541
|
+
server = start(port=1080, version="9.0.0")
|
|
460
542
|
# ...
|
|
461
543
|
server.stop()
|
|
462
544
|
```
|
|
@@ -469,6 +551,8 @@ server.stop()
|
|
|
469
551
|
| `start(port, version=None, *, extra_args=None, log=True)` | Ensure the binary and start MockServer. Returns a `MockServerProcess`. |
|
|
470
552
|
| `MockServerProcess` | Handle to the running process. Properties: `port`, `pid`, `launcher`, `returncode`. Methods: `stop(timeout=10.0)`. Supports `with` statement. |
|
|
471
553
|
|
|
554
|
+
On Windows the launcher runs `mockserver.bat` through `cmd.exe` with every argument quoted, so characters such as `&`, `|` and `^` reach MockServer unchanged. `start()` raises `ValueError` if the launcher path or an `extra_args` entry contains a double quote, a line break or NUL, or if together they contain more than one `%` (which `cmd.exe` would expand as a `%NAME%` variable). No shell is used on Linux or macOS.
|
|
555
|
+
|
|
472
556
|
### Supported platforms
|
|
473
557
|
|
|
474
558
|
| OS | Architecture |
|
|
@@ -2,9 +2,11 @@ from mockserver.models import (
|
|
|
2
2
|
AfterAction,
|
|
3
3
|
AllOfBody,
|
|
4
4
|
BinaryResponse,
|
|
5
|
+
BinaryUpstream,
|
|
5
6
|
Body,
|
|
6
7
|
CaptureRule,
|
|
7
8
|
CaptureSource,
|
|
9
|
+
ConditionalRequestDefinition,
|
|
8
10
|
ConnectionOptions,
|
|
9
11
|
CrossProtocolScenario,
|
|
10
12
|
CrossProtocolTrigger,
|
|
@@ -19,6 +21,7 @@ from mockserver.models import (
|
|
|
19
21
|
ExpectationId,
|
|
20
22
|
ExpectationStep,
|
|
21
23
|
GraphQLBody,
|
|
24
|
+
GraphQLSubscriptionFilter,
|
|
22
25
|
GrpcBidiResponse,
|
|
23
26
|
GrpcBidiRule,
|
|
24
27
|
GrpcStreamMessage,
|
|
@@ -47,12 +50,14 @@ from mockserver.models import (
|
|
|
47
50
|
Protocol,
|
|
48
51
|
RateLimit,
|
|
49
52
|
RateLimitAlgorithm,
|
|
53
|
+
RecoverAfter,
|
|
50
54
|
ValidationMode,
|
|
51
55
|
WasmBody,
|
|
52
56
|
XmlBody,
|
|
53
57
|
XmlSchemaBody,
|
|
54
58
|
XPathBody,
|
|
55
59
|
LoadCapture,
|
|
60
|
+
LoadCheck,
|
|
56
61
|
LoadFeeder,
|
|
57
62
|
LoadPacing,
|
|
58
63
|
LoadProfile,
|
|
@@ -77,6 +82,17 @@ from mockserver.models import (
|
|
|
77
82
|
VerificationTimes,
|
|
78
83
|
WebSocketFrameMatcher,
|
|
79
84
|
WebSocketMessage,
|
|
85
|
+
Arrival,
|
|
86
|
+
ArrivalForwardFailure,
|
|
87
|
+
ClosestMatch,
|
|
88
|
+
DebugMismatchResult,
|
|
89
|
+
ExplainedExpectation,
|
|
90
|
+
ExplainedRequest,
|
|
91
|
+
ExplainUnmatchedResult,
|
|
92
|
+
ExplanationNarration,
|
|
93
|
+
RequestExplanation,
|
|
94
|
+
SuggestedFix,
|
|
95
|
+
UnmatchedRequest,
|
|
80
96
|
)
|
|
81
97
|
from mockserver.llm import (
|
|
82
98
|
Completion,
|
|
@@ -151,9 +167,11 @@ __all__ = [
|
|
|
151
167
|
"AllOfBody",
|
|
152
168
|
"AsyncMockServerClient",
|
|
153
169
|
"BinaryResponse",
|
|
170
|
+
"BinaryUpstream",
|
|
154
171
|
"Body",
|
|
155
172
|
"CaptureRule",
|
|
156
173
|
"CaptureSource",
|
|
174
|
+
"ConditionalRequestDefinition",
|
|
157
175
|
"ConnectionOptions",
|
|
158
176
|
"CrossProtocolScenario",
|
|
159
177
|
"CrossProtocolTrigger",
|
|
@@ -168,6 +186,7 @@ __all__ = [
|
|
|
168
186
|
"ExpectationId",
|
|
169
187
|
"ExpectationStep",
|
|
170
188
|
"GraphQLBody",
|
|
189
|
+
"GraphQLSubscriptionFilter",
|
|
171
190
|
"GrpcBidiResponse",
|
|
172
191
|
"GrpcBidiRule",
|
|
173
192
|
"GrpcStreamMessage",
|
|
@@ -196,12 +215,14 @@ __all__ = [
|
|
|
196
215
|
"Protocol",
|
|
197
216
|
"RateLimit",
|
|
198
217
|
"RateLimitAlgorithm",
|
|
218
|
+
"RecoverAfter",
|
|
199
219
|
"ValidationMode",
|
|
200
220
|
"WasmBody",
|
|
201
221
|
"XmlBody",
|
|
202
222
|
"XmlSchemaBody",
|
|
203
223
|
"XPathBody",
|
|
204
224
|
"LoadCapture",
|
|
225
|
+
"LoadCheck",
|
|
205
226
|
"LoadFeeder",
|
|
206
227
|
"LoadPacing",
|
|
207
228
|
"LoadProfile",
|
|
@@ -276,6 +297,17 @@ __all__ = [
|
|
|
276
297
|
"VerificationTimes",
|
|
277
298
|
"WebSocketFrameMatcher",
|
|
278
299
|
"WebSocketMessage",
|
|
300
|
+
"Arrival",
|
|
301
|
+
"ArrivalForwardFailure",
|
|
302
|
+
"ClosestMatch",
|
|
303
|
+
"DebugMismatchResult",
|
|
304
|
+
"ExplainedExpectation",
|
|
305
|
+
"ExplainedRequest",
|
|
306
|
+
"ExplainUnmatchedResult",
|
|
307
|
+
"ExplanationNarration",
|
|
308
|
+
"RequestExplanation",
|
|
309
|
+
"SuggestedFix",
|
|
310
|
+
"UnmatchedRequest",
|
|
279
311
|
"MockServerProcess",
|
|
280
312
|
"bundle_base_name",
|
|
281
313
|
"cache_dir",
|
|
@@ -428,7 +428,7 @@ class A2aMockBuilder:
|
|
|
428
428
|
return Expectation(
|
|
429
429
|
http_request=self._json_rpc_request("tasks/send"),
|
|
430
430
|
http_override_forwarded_request=HttpOverrideForwardedRequest(
|
|
431
|
-
|
|
431
|
+
request_override=webhook_request,
|
|
432
432
|
response_template=client_response_template,
|
|
433
433
|
),
|
|
434
434
|
)
|
|
@@ -18,7 +18,11 @@ from mockserver.exceptions import (
|
|
|
18
18
|
)
|
|
19
19
|
from mockserver.fluent import ForwardChainExpectation
|
|
20
20
|
from mockserver.models import (
|
|
21
|
+
ConditionalRequestDefinition,
|
|
22
|
+
DebugMismatchResult,
|
|
23
|
+
DnsRequestDefinition,
|
|
21
24
|
Expectation,
|
|
25
|
+
ExplainUnmatchedResult,
|
|
22
26
|
HttpChaosProfile,
|
|
23
27
|
HttpObjectCallback,
|
|
24
28
|
HttpRequest,
|
|
@@ -27,11 +31,13 @@ from mockserver.models import (
|
|
|
27
31
|
LoadScenario,
|
|
28
32
|
OpenAPIExpectation,
|
|
29
33
|
Ports,
|
|
34
|
+
RequestExplanation,
|
|
30
35
|
TimeToLive,
|
|
31
36
|
Times,
|
|
32
37
|
Verification,
|
|
33
38
|
VerificationSequence,
|
|
34
39
|
VerificationTimes,
|
|
40
|
+
_deserialize_recorded_request,
|
|
35
41
|
)
|
|
36
42
|
from mockserver.websocket_client import MockServerWebSocketClient
|
|
37
43
|
|
|
@@ -757,18 +763,21 @@ class AsyncMockServerClient:
|
|
|
757
763
|
Returns the verdict dict with a ``result`` of ``PASS`` or ``INCONCLUSIVE``
|
|
758
764
|
(HTTP 200). A ``FAIL`` verdict (HTTP 406) raises
|
|
759
765
|
:class:`MockServerVerificationError` so a CI or chaos gate can assert on it
|
|
760
|
-
directly. SLO tracking is off by default — the server returns
|
|
761
|
-
:class:`MockServerError`) until started with ``sloTrackingEnabled=true
|
|
766
|
+
directly. SLO tracking is off by default — the server returns 403 (raised as
|
|
767
|
+
:class:`MockServerError`) until started with ``sloTrackingEnabled=true``;
|
|
768
|
+
malformed criteria are a separate 400.
|
|
762
769
|
"""
|
|
763
770
|
body = json.dumps(criteria)
|
|
764
771
|
status, response_body = await self._request("PUT", "/mockserver/verifySLO", body)
|
|
765
772
|
if status == 406:
|
|
766
773
|
raise MockServerVerificationError(response_body or "SLO verdict: FAIL")
|
|
767
|
-
if status ==
|
|
774
|
+
if status == 403:
|
|
768
775
|
raise MockServerError(
|
|
769
|
-
"
|
|
770
|
-
f"sloTrackingEnabled=true
|
|
776
|
+
"SLO tracking is disabled — start MockServer with "
|
|
777
|
+
f"sloTrackingEnabled=true: {response_body}"
|
|
771
778
|
)
|
|
779
|
+
if status == 400:
|
|
780
|
+
raise MockServerError(f"Invalid SLO criteria: {response_body}")
|
|
772
781
|
if status >= 400:
|
|
773
782
|
raise MockServerError(
|
|
774
783
|
f"Failed to verify SLO (status={status}): {response_body}"
|
|
@@ -1114,7 +1123,7 @@ class AsyncMockServerClient:
|
|
|
1114
1123
|
|
|
1115
1124
|
async def verify(
|
|
1116
1125
|
self,
|
|
1117
|
-
request: HttpRequest | None = None,
|
|
1126
|
+
request: HttpRequest | ConditionalRequestDefinition | None = None,
|
|
1118
1127
|
times: VerificationTimes | None = None,
|
|
1119
1128
|
*,
|
|
1120
1129
|
response: HttpResponse | None = None,
|
|
@@ -1131,7 +1140,7 @@ class AsyncMockServerClient:
|
|
|
1131
1140
|
|
|
1132
1141
|
async def verify_sequence(
|
|
1133
1142
|
self,
|
|
1134
|
-
*requests: HttpRequest,
|
|
1143
|
+
*requests: HttpRequest | ConditionalRequestDefinition,
|
|
1135
1144
|
responses: list[HttpResponse] | None = None,
|
|
1136
1145
|
) -> None:
|
|
1137
1146
|
verification = VerificationSequence(
|
|
@@ -1156,8 +1165,12 @@ class AsyncMockServerClient:
|
|
|
1156
1165
|
)
|
|
1157
1166
|
|
|
1158
1167
|
async def retrieve_recorded_requests(
|
|
1159
|
-
self, request: HttpRequest | None = None
|
|
1160
|
-
) -> list[HttpRequest]:
|
|
1168
|
+
self, request: HttpRequest | DnsRequestDefinition | None = None
|
|
1169
|
+
) -> list[HttpRequest | DnsRequestDefinition | dict]:
|
|
1170
|
+
"""Retrieve the recorded requests matching ``request`` (all HTTP requests when
|
|
1171
|
+
omitted). With a DNS filter the DNS queries come back as
|
|
1172
|
+
:class:`DnsRequestDefinition`; a binary message comes back as its raw dict.
|
|
1173
|
+
"""
|
|
1161
1174
|
body = json.dumps(request.to_dict()) if request else ""
|
|
1162
1175
|
status, response_body = await self._request(
|
|
1163
1176
|
"PUT",
|
|
@@ -1172,7 +1185,7 @@ class AsyncMockServerClient:
|
|
|
1172
1185
|
if response_body:
|
|
1173
1186
|
parsed = json.loads(response_body)
|
|
1174
1187
|
if isinstance(parsed, list):
|
|
1175
|
-
return [
|
|
1188
|
+
return [_deserialize_recorded_request(r) for r in parsed]
|
|
1176
1189
|
return []
|
|
1177
1190
|
|
|
1178
1191
|
async def retrieve_active_expectations(
|
|
@@ -1215,6 +1228,176 @@ class AsyncMockServerClient:
|
|
|
1215
1228
|
return [Expectation.from_dict(e) for e in parsed]
|
|
1216
1229
|
return []
|
|
1217
1230
|
|
|
1231
|
+
async def _retrieve_by_id(
|
|
1232
|
+
self, expectation_id: str, query_params: dict[str, str]
|
|
1233
|
+
) -> tuple[int, str]:
|
|
1234
|
+
"""Send a retrieve request filtered by expectation id.
|
|
1235
|
+
|
|
1236
|
+
An expectation id may be sent in place of a request matcher. For
|
|
1237
|
+
``ACTIVE_EXPECTATIONS`` the server returns only the expectation with that
|
|
1238
|
+
id; for every other type it returns the entries matching that
|
|
1239
|
+
expectation's request, exactly as verify by expectation id matches them.
|
|
1240
|
+
An unknown id is rejected with a 400.
|
|
1241
|
+
"""
|
|
1242
|
+
return await self._request(
|
|
1243
|
+
"PUT",
|
|
1244
|
+
"/mockserver/retrieve",
|
|
1245
|
+
json.dumps({"id": expectation_id}),
|
|
1246
|
+
query_params,
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
async def retrieve_recorded_requests_by_id(
|
|
1250
|
+
self, expectation_id: str
|
|
1251
|
+
) -> list[HttpRequest | DnsRequestDefinition | dict]:
|
|
1252
|
+
status, response_body = await self._retrieve_by_id(
|
|
1253
|
+
expectation_id, {"type": "REQUESTS", "format": "JSON"}
|
|
1254
|
+
)
|
|
1255
|
+
if status >= 400:
|
|
1256
|
+
raise MockServerError(
|
|
1257
|
+
f"Failed to retrieve recorded requests by id (status={status}): {response_body}"
|
|
1258
|
+
)
|
|
1259
|
+
if response_body:
|
|
1260
|
+
parsed = json.loads(response_body)
|
|
1261
|
+
if isinstance(parsed, list):
|
|
1262
|
+
return [_deserialize_recorded_request(r) for r in parsed]
|
|
1263
|
+
return []
|
|
1264
|
+
|
|
1265
|
+
async def retrieve_active_expectations_by_id(
|
|
1266
|
+
self, expectation_id: str
|
|
1267
|
+
) -> list[Expectation]:
|
|
1268
|
+
status, response_body = await self._retrieve_by_id(
|
|
1269
|
+
expectation_id, {"type": "ACTIVE_EXPECTATIONS", "format": "JSON"}
|
|
1270
|
+
)
|
|
1271
|
+
if status >= 400:
|
|
1272
|
+
raise MockServerError(
|
|
1273
|
+
f"Failed to retrieve active expectations by id (status={status}): {response_body}"
|
|
1274
|
+
)
|
|
1275
|
+
if response_body:
|
|
1276
|
+
parsed = json.loads(response_body)
|
|
1277
|
+
if isinstance(parsed, list):
|
|
1278
|
+
return [Expectation.from_dict(e) for e in parsed]
|
|
1279
|
+
return []
|
|
1280
|
+
|
|
1281
|
+
async def retrieve_recorded_expectations_by_id(
|
|
1282
|
+
self, expectation_id: str
|
|
1283
|
+
) -> list[Expectation]:
|
|
1284
|
+
status, response_body = await self._retrieve_by_id(
|
|
1285
|
+
expectation_id, {"type": "RECORDED_EXPECTATIONS", "format": "JSON"}
|
|
1286
|
+
)
|
|
1287
|
+
if status >= 400:
|
|
1288
|
+
raise MockServerError(
|
|
1289
|
+
f"Failed to retrieve recorded expectations by id (status={status}): {response_body}"
|
|
1290
|
+
)
|
|
1291
|
+
if response_body:
|
|
1292
|
+
parsed = json.loads(response_body)
|
|
1293
|
+
if isinstance(parsed, list):
|
|
1294
|
+
return [Expectation.from_dict(e) for e in parsed]
|
|
1295
|
+
return []
|
|
1296
|
+
|
|
1297
|
+
async def retrieve_recorded_requests_and_responses_by_id(
|
|
1298
|
+
self, expectation_id: str
|
|
1299
|
+
) -> list[HttpRequestAndHttpResponse]:
|
|
1300
|
+
status, response_body = await self._retrieve_by_id(
|
|
1301
|
+
expectation_id, {"type": "REQUEST_RESPONSES", "format": "JSON"}
|
|
1302
|
+
)
|
|
1303
|
+
if status >= 400:
|
|
1304
|
+
raise MockServerError(
|
|
1305
|
+
f"Failed to retrieve request/responses by id (status={status}): {response_body}"
|
|
1306
|
+
)
|
|
1307
|
+
if response_body:
|
|
1308
|
+
parsed = json.loads(response_body)
|
|
1309
|
+
if isinstance(parsed, list):
|
|
1310
|
+
return [HttpRequestAndHttpResponse.from_dict(rr) for rr in parsed]
|
|
1311
|
+
return []
|
|
1312
|
+
|
|
1313
|
+
async def retrieve_log_messages_by_id(self, expectation_id: str) -> list[str]:
|
|
1314
|
+
status, response_body = await self._retrieve_by_id(
|
|
1315
|
+
expectation_id, {"type": "LOGS"}
|
|
1316
|
+
)
|
|
1317
|
+
if status >= 400:
|
|
1318
|
+
raise MockServerError(
|
|
1319
|
+
f"Failed to retrieve log messages by id (status={status}): {response_body}"
|
|
1320
|
+
)
|
|
1321
|
+
if response_body:
|
|
1322
|
+
try:
|
|
1323
|
+
parsed = json.loads(response_body)
|
|
1324
|
+
if isinstance(parsed, list):
|
|
1325
|
+
return parsed
|
|
1326
|
+
except json.JSONDecodeError:
|
|
1327
|
+
return response_body.split("------------------------------------\n")
|
|
1328
|
+
return []
|
|
1329
|
+
|
|
1330
|
+
async def _diagnostic(
|
|
1331
|
+
self, path: str, body: str | None, query_params: dict[str, str] | None, failure: str
|
|
1332
|
+
) -> dict:
|
|
1333
|
+
status, response_body = await self._request("PUT", path, body, query_params)
|
|
1334
|
+
try:
|
|
1335
|
+
parsed = json.loads(response_body) if response_body else {}
|
|
1336
|
+
except json.JSONDecodeError:
|
|
1337
|
+
parsed = None
|
|
1338
|
+
if status != 200 or not isinstance(parsed, dict):
|
|
1339
|
+
reason = parsed.get("error") if isinstance(parsed, dict) else None
|
|
1340
|
+
raise MockServerError(f"{failure} (status={status}): {reason or response_body}")
|
|
1341
|
+
return parsed
|
|
1342
|
+
|
|
1343
|
+
async def explain(self, correlation_id: str) -> RequestExplanation:
|
|
1344
|
+
"""Explain how a recorded request would be served now.
|
|
1345
|
+
|
|
1346
|
+
Issues ``PUT /mockserver/explain?correlationId=<id>``, which re-checks the
|
|
1347
|
+
request with that correlation id against the current expectations, in the
|
|
1348
|
+
order MockServer evaluates them, and returns the expectation that would
|
|
1349
|
+
serve it, why each expectation ahead of it was passed over, and which
|
|
1350
|
+
lower-priority expectations it shadows. Read-only: it consumes no Times
|
|
1351
|
+
and moves no scenario.
|
|
1352
|
+
|
|
1353
|
+
Raises :class:`ValueError` for a blank id, and :class:`MockServerError`
|
|
1354
|
+
with the server's reason when no request is recorded with this id (never
|
|
1355
|
+
received, cleared or evicted) or it is not an HTTP request.
|
|
1356
|
+
"""
|
|
1357
|
+
if not correlation_id or not correlation_id.strip():
|
|
1358
|
+
raise ValueError("explain requires a non blank correlation_id")
|
|
1359
|
+
data = await self._diagnostic(
|
|
1360
|
+
"/mockserver/explain", None, {"correlationId": correlation_id},
|
|
1361
|
+
f'explain failed for correlationId "{correlation_id}"',
|
|
1362
|
+
)
|
|
1363
|
+
return RequestExplanation.from_dict(data)
|
|
1364
|
+
|
|
1365
|
+
async def debug_mismatch(self, request: HttpRequest | dict) -> DebugMismatchResult:
|
|
1366
|
+
"""Compare a request with every active expectation.
|
|
1367
|
+
|
|
1368
|
+
Issues ``PUT /mockserver/debugMismatch`` and returns, for each expectation,
|
|
1369
|
+
whether it matches and the per-field differences, plus the closest match.
|
|
1370
|
+
Only an HTTP request is supported; any other definition raises
|
|
1371
|
+
:class:`MockServerError`.
|
|
1372
|
+
"""
|
|
1373
|
+
payload = request.to_dict() if hasattr(request, "to_dict") else request
|
|
1374
|
+
data = await self._diagnostic(
|
|
1375
|
+
"/mockserver/debugMismatch", json.dumps(payload), None, "debugMismatch failed"
|
|
1376
|
+
)
|
|
1377
|
+
return DebugMismatchResult.from_dict(data)
|
|
1378
|
+
|
|
1379
|
+
async def explain_unmatched(
|
|
1380
|
+
self, limit: int | None = None, suggest_fixes: bool | None = None
|
|
1381
|
+
) -> ExplainUnmatchedResult:
|
|
1382
|
+
"""Explain why recent requests matched no expectation.
|
|
1383
|
+
|
|
1384
|
+
Issues ``PUT /mockserver/explainUnmatched`` and returns each recent
|
|
1385
|
+
unmatched request with its closest expectations, closest first. ``limit``
|
|
1386
|
+
bounds how many requests are analysed (server default 10);
|
|
1387
|
+
``suggest_fixes`` adds a suggested JSON patch to each of the five closest
|
|
1388
|
+
non-matching expectations (nothing is applied).
|
|
1389
|
+
"""
|
|
1390
|
+
options: dict[str, Any] = {}
|
|
1391
|
+
if limit is not None:
|
|
1392
|
+
options["limit"] = limit
|
|
1393
|
+
if suggest_fixes is not None:
|
|
1394
|
+
options["suggestFixes"] = suggest_fixes
|
|
1395
|
+
data = await self._diagnostic(
|
|
1396
|
+
"/mockserver/explainUnmatched", json.dumps(options) if options else None, None,
|
|
1397
|
+
"explainUnmatched failed",
|
|
1398
|
+
)
|
|
1399
|
+
return ExplainUnmatchedResult.from_dict(data)
|
|
1400
|
+
|
|
1218
1401
|
async def retrieve_expectations_as_code(
|
|
1219
1402
|
self, fmt: str = "java", request: HttpRequest | None = None
|
|
1220
1403
|
) -> str:
|
|
@@ -1333,7 +1516,7 @@ class AsyncMockServerClient:
|
|
|
1333
1516
|
|
|
1334
1517
|
def when(
|
|
1335
1518
|
self,
|
|
1336
|
-
request: HttpRequest,
|
|
1519
|
+
request: HttpRequest | ConditionalRequestDefinition,
|
|
1337
1520
|
times: Times | None = None,
|
|
1338
1521
|
time_to_live: TimeToLive | None = None,
|
|
1339
1522
|
priority: int | None = None,
|