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 ADDED
@@ -0,0 +1,85 @@
1
+ from mockserver.models import (
2
+ AfterAction,
3
+ Body,
4
+ ConnectionOptions,
5
+ Delay,
6
+ DelayDistribution,
7
+ Expectation,
8
+ ExpectationId,
9
+ HttpClassCallback,
10
+ HttpError,
11
+ HttpForward,
12
+ HttpObjectCallback,
13
+ HttpOverrideForwardedRequest,
14
+ HttpRequest,
15
+ HttpRequestAndHttpResponse,
16
+ HttpResponse,
17
+ HttpSseResponse,
18
+ HttpTemplate,
19
+ HttpWebSocketResponse,
20
+ JsonRpcBody,
21
+ KeyToMultiValue,
22
+ OpenAPIDefinition,
23
+ OpenAPIExpectation,
24
+ Ports,
25
+ RequestDefinition,
26
+ SocketAddress,
27
+ SseEvent,
28
+ TimeToLive,
29
+ Times,
30
+ Verification,
31
+ VerificationSequence,
32
+ VerificationTimes,
33
+ WebSocketMessage,
34
+ )
35
+ from mockserver.exceptions import (
36
+ MockServerCallbackError,
37
+ MockServerConnectionError,
38
+ MockServerError,
39
+ MockServerVerificationError,
40
+ MockServerWebSocketError,
41
+ )
42
+ from mockserver.client import MockServerClient
43
+ from mockserver.async_client import AsyncMockServerClient
44
+
45
+ __all__ = [
46
+ "AfterAction",
47
+ "AsyncMockServerClient",
48
+ "Body",
49
+ "ConnectionOptions",
50
+ "Delay",
51
+ "DelayDistribution",
52
+ "Expectation",
53
+ "ExpectationId",
54
+ "HttpClassCallback",
55
+ "HttpError",
56
+ "HttpForward",
57
+ "HttpObjectCallback",
58
+ "HttpOverrideForwardedRequest",
59
+ "HttpRequest",
60
+ "HttpRequestAndHttpResponse",
61
+ "HttpResponse",
62
+ "HttpSseResponse",
63
+ "HttpTemplate",
64
+ "HttpWebSocketResponse",
65
+ "JsonRpcBody",
66
+ "KeyToMultiValue",
67
+ "MockServerCallbackError",
68
+ "MockServerClient",
69
+ "MockServerConnectionError",
70
+ "MockServerError",
71
+ "MockServerVerificationError",
72
+ "MockServerWebSocketError",
73
+ "OpenAPIDefinition",
74
+ "OpenAPIExpectation",
75
+ "Ports",
76
+ "RequestDefinition",
77
+ "SocketAddress",
78
+ "SseEvent",
79
+ "TimeToLive",
80
+ "Times",
81
+ "Verification",
82
+ "VerificationSequence",
83
+ "VerificationTimes",
84
+ "WebSocketMessage",
85
+ ]
@@ -0,0 +1,425 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import socket
7
+ import ssl
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from typing import Any, Callable
12
+
13
+ from mockserver.exceptions import (
14
+ MockServerConnectionError,
15
+ MockServerError,
16
+ MockServerVerificationError,
17
+ )
18
+ from mockserver.fluent import ForwardChainExpectation
19
+ from mockserver.models import (
20
+ Expectation,
21
+ HttpObjectCallback,
22
+ HttpRequest,
23
+ HttpRequestAndHttpResponse,
24
+ HttpResponse,
25
+ OpenAPIExpectation,
26
+ Ports,
27
+ TimeToLive,
28
+ Times,
29
+ Verification,
30
+ VerificationSequence,
31
+ VerificationTimes,
32
+ )
33
+ from mockserver.websocket_client import MockServerWebSocketClient
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class AsyncMockServerClient:
39
+ def __init__(
40
+ self,
41
+ host: str,
42
+ port: int,
43
+ context_path: str = "",
44
+ secure: bool = False,
45
+ ca_cert_path: str | None = None,
46
+ tls_verify: bool = True,
47
+ ) -> None:
48
+ self._host = host
49
+ self._port = port
50
+ self._context_path = context_path
51
+ self._secure = secure
52
+ self._ca_cert_path = ca_cert_path
53
+ self._tls_verify = tls_verify
54
+ self._websocket_clients: list[MockServerWebSocketClient] = []
55
+
56
+ scheme = "https" if secure else "http"
57
+ ctx_path = ""
58
+ if context_path:
59
+ ctx_path = context_path if context_path.startswith("/") else f"/{context_path}"
60
+ self._base_url = f"{scheme}://{host}:{port}{ctx_path}"
61
+
62
+ self._ssl_context: ssl.SSLContext | None = None
63
+ if secure:
64
+ self._ssl_context = ssl.create_default_context()
65
+ if ca_cert_path:
66
+ self._ssl_context.load_verify_locations(ca_cert_path)
67
+ elif not tls_verify:
68
+ self._ssl_context.check_hostname = False
69
+ self._ssl_context.verify_mode = ssl.CERT_NONE
70
+
71
+ async def _request(
72
+ self,
73
+ method: str,
74
+ path: str,
75
+ body: str | None = None,
76
+ query_params: dict[str, str] | None = None,
77
+ ) -> tuple[int, str]:
78
+ url = f"{self._base_url}{path}"
79
+ if query_params:
80
+ url = f"{url}?{urllib.parse.urlencode(query_params)}"
81
+
82
+ data = body.encode("utf-8") if body else None
83
+ req = urllib.request.Request(url, data=data, method=method)
84
+ req.add_header("Content-Type", "application/json; charset=utf-8")
85
+
86
+ def _do_request() -> tuple[int, str]:
87
+ try:
88
+ response = urllib.request.urlopen(
89
+ req, context=self._ssl_context, timeout=60
90
+ )
91
+ return response.status, response.read().decode("utf-8")
92
+ except urllib.error.HTTPError as e:
93
+ return e.code, e.read().decode("utf-8")
94
+ except socket.timeout as e:
95
+ raise MockServerConnectionError(
96
+ f"Request to MockServer at {self._base_url} timed out: {e}"
97
+ ) from e
98
+ except (urllib.error.URLError, OSError) as e:
99
+ raise MockServerConnectionError(
100
+ f"Failed to connect to MockServer at {self._base_url}: {e}"
101
+ ) from e
102
+
103
+ return await asyncio.to_thread(_do_request)
104
+
105
+ async def upsert(self, *expectations: Expectation) -> list[Expectation]:
106
+ body = json.dumps([e.to_dict() for e in expectations])
107
+ status, response_body = await self._request("PUT", "/mockserver/expectation", body)
108
+ if status == 400:
109
+ raise MockServerError(f"Invalid expectation: {response_body}")
110
+ if status >= 400:
111
+ raise MockServerError(
112
+ f"Failed to upsert expectations (status={status}): {response_body}"
113
+ )
114
+ if response_body:
115
+ parsed = json.loads(response_body)
116
+ if isinstance(parsed, list):
117
+ return [Expectation.from_dict(e) for e in parsed]
118
+ return list(expectations)
119
+
120
+ async def open_api_expectation(self, expectation: OpenAPIExpectation) -> None:
121
+ body = json.dumps(expectation.to_dict())
122
+ status, response_body = await self._request("PUT", "/mockserver/openapi", body)
123
+ if status >= 400:
124
+ raise MockServerError(
125
+ f"Failed to create OpenAPI expectation (status={status}): {response_body}"
126
+ )
127
+
128
+ async def clear(
129
+ self,
130
+ request: HttpRequest | None = None,
131
+ clear_type: str | None = None,
132
+ ) -> None:
133
+ query_params = {}
134
+ if clear_type:
135
+ query_params["type"] = clear_type
136
+ body = json.dumps(request.to_dict()) if request else ""
137
+ status, response_body = await self._request(
138
+ "PUT", "/mockserver/clear", body, query_params or None
139
+ )
140
+ if status >= 400:
141
+ raise MockServerError(
142
+ f"Failed to clear (status={status}): {response_body}"
143
+ )
144
+
145
+ async def clear_by_id(
146
+ self,
147
+ expectation_id: str,
148
+ clear_type: str | None = None,
149
+ ) -> None:
150
+ query_params = {}
151
+ if clear_type:
152
+ query_params["type"] = clear_type
153
+ body = json.dumps({"id": expectation_id})
154
+ status, response_body = await self._request(
155
+ "PUT", "/mockserver/clear", body, query_params or None
156
+ )
157
+ if status >= 400:
158
+ raise MockServerError(
159
+ f"Failed to clear by id (status={status}): {response_body}"
160
+ )
161
+
162
+ async def reset(self) -> None:
163
+ status, response_body = await self._request("PUT", "/mockserver/reset")
164
+ if status >= 400:
165
+ raise MockServerError(
166
+ f"Failed to reset (status={status}): {response_body}"
167
+ )
168
+
169
+ async def verify(
170
+ self,
171
+ request: HttpRequest,
172
+ times: VerificationTimes | None = None,
173
+ ) -> None:
174
+ verification = Verification(http_request=request, times=times)
175
+ body = json.dumps(verification.to_dict())
176
+ status, response_body = await self._request("PUT", "/mockserver/verify", body)
177
+ if status == 406:
178
+ raise MockServerVerificationError(response_body)
179
+ if status >= 400:
180
+ raise MockServerError(
181
+ f"Failed to verify (status={status}): {response_body}"
182
+ )
183
+
184
+ async def verify_sequence(self, *requests: HttpRequest) -> None:
185
+ verification = VerificationSequence(
186
+ http_requests=list(requests)
187
+ )
188
+ body = json.dumps(verification.to_dict())
189
+ status, response_body = await self._request(
190
+ "PUT", "/mockserver/verifySequence", body
191
+ )
192
+ if status == 406:
193
+ raise MockServerVerificationError(response_body)
194
+ if status >= 400:
195
+ raise MockServerError(
196
+ f"Failed to verify sequence (status={status}): {response_body}"
197
+ )
198
+
199
+ async def verify_zero_interactions(self) -> None:
200
+ await self.verify(
201
+ HttpRequest(),
202
+ VerificationTimes(at_most=0),
203
+ )
204
+
205
+ async def retrieve_recorded_requests(
206
+ self, request: HttpRequest | None = None
207
+ ) -> list[HttpRequest]:
208
+ body = json.dumps(request.to_dict()) if request else ""
209
+ status, response_body = await self._request(
210
+ "PUT",
211
+ "/mockserver/retrieve",
212
+ body,
213
+ {"type": "REQUESTS", "format": "JSON"},
214
+ )
215
+ if status >= 400:
216
+ raise MockServerError(
217
+ f"Failed to retrieve recorded requests (status={status}): {response_body}"
218
+ )
219
+ if response_body:
220
+ parsed = json.loads(response_body)
221
+ if isinstance(parsed, list):
222
+ return [HttpRequest.from_dict(r) for r in parsed]
223
+ return []
224
+
225
+ async def retrieve_active_expectations(
226
+ self, request: HttpRequest | None = None
227
+ ) -> list[Expectation]:
228
+ body = json.dumps(request.to_dict()) if request else ""
229
+ status, response_body = await self._request(
230
+ "PUT",
231
+ "/mockserver/retrieve",
232
+ body,
233
+ {"type": "ACTIVE_EXPECTATIONS", "format": "JSON"},
234
+ )
235
+ if status >= 400:
236
+ raise MockServerError(
237
+ f"Failed to retrieve active expectations (status={status}): {response_body}"
238
+ )
239
+ if response_body:
240
+ parsed = json.loads(response_body)
241
+ if isinstance(parsed, list):
242
+ return [Expectation.from_dict(e) for e in parsed]
243
+ return []
244
+
245
+ async def retrieve_recorded_expectations(
246
+ self, request: HttpRequest | None = None
247
+ ) -> list[Expectation]:
248
+ body = json.dumps(request.to_dict()) if request else ""
249
+ status, response_body = await self._request(
250
+ "PUT",
251
+ "/mockserver/retrieve",
252
+ body,
253
+ {"type": "RECORDED_EXPECTATIONS", "format": "JSON"},
254
+ )
255
+ if status >= 400:
256
+ raise MockServerError(
257
+ f"Failed to retrieve recorded expectations (status={status}): {response_body}"
258
+ )
259
+ if response_body:
260
+ parsed = json.loads(response_body)
261
+ if isinstance(parsed, list):
262
+ return [Expectation.from_dict(e) for e in parsed]
263
+ return []
264
+
265
+ async def retrieve_recorded_requests_and_responses(
266
+ self, request: HttpRequest | None = None
267
+ ) -> list[HttpRequestAndHttpResponse]:
268
+ body = json.dumps(request.to_dict()) if request else ""
269
+ status, response_body = await self._request(
270
+ "PUT",
271
+ "/mockserver/retrieve",
272
+ body,
273
+ {"type": "REQUEST_RESPONSES", "format": "JSON"},
274
+ )
275
+ if status >= 400:
276
+ raise MockServerError(
277
+ f"Failed to retrieve request/responses (status={status}): {response_body}"
278
+ )
279
+ if response_body:
280
+ parsed = json.loads(response_body)
281
+ if isinstance(parsed, list):
282
+ return [HttpRequestAndHttpResponse.from_dict(rr) for rr in parsed]
283
+ return []
284
+
285
+ async def retrieve_log_messages(
286
+ self, request: HttpRequest | None = None
287
+ ) -> list[str]:
288
+ body = json.dumps(request.to_dict()) if request else ""
289
+ status, response_body = await self._request(
290
+ "PUT",
291
+ "/mockserver/retrieve",
292
+ body,
293
+ {"type": "LOGS"},
294
+ )
295
+ if status >= 400:
296
+ raise MockServerError(
297
+ f"Failed to retrieve log messages (status={status}): {response_body}"
298
+ )
299
+ if response_body:
300
+ try:
301
+ parsed = json.loads(response_body)
302
+ if isinstance(parsed, list):
303
+ return parsed
304
+ except json.JSONDecodeError:
305
+ return response_body.split("------------------------------------\n")
306
+ return []
307
+
308
+ async def bind(self, *ports: int) -> list[int]:
309
+ body = json.dumps(Ports(ports=list(ports)).to_dict())
310
+ status, response_body = await self._request("PUT", "/mockserver/bind", body)
311
+ if status >= 400:
312
+ raise MockServerError(
313
+ f"Failed to bind ports (status={status}): {response_body}"
314
+ )
315
+ if response_body:
316
+ parsed = json.loads(response_body)
317
+ return Ports.from_dict(parsed).ports
318
+ return []
319
+
320
+ async def stop(self) -> None:
321
+ try:
322
+ await self._request("PUT", "/mockserver/stop")
323
+ except MockServerConnectionError:
324
+ pass
325
+
326
+ async def has_started(self, attempts: int = 10, timeout: float = 0.5) -> bool:
327
+ for i in range(attempts):
328
+ try:
329
+ status, _ = await self._request("PUT", "/mockserver/status")
330
+ if status == 200:
331
+ return True
332
+ except MockServerConnectionError:
333
+ pass
334
+ if i < attempts - 1:
335
+ await asyncio.sleep(timeout)
336
+ return False
337
+
338
+ def when(
339
+ self,
340
+ request: HttpRequest,
341
+ times: Times | None = None,
342
+ time_to_live: TimeToLive | None = None,
343
+ priority: int | None = None,
344
+ ) -> ForwardChainExpectation:
345
+ expectation = Expectation(
346
+ http_request=request,
347
+ times=times,
348
+ time_to_live=time_to_live,
349
+ priority=priority,
350
+ )
351
+ return ForwardChainExpectation(self, expectation)
352
+
353
+ async def mock_with_callback(
354
+ self,
355
+ request: HttpRequest,
356
+ callback: Callable,
357
+ times: Times | None = None,
358
+ time_to_live: TimeToLive | None = None,
359
+ ) -> list[Expectation]:
360
+ client_id = await self._register_websocket_callback("response", callback)
361
+ expectation = Expectation(
362
+ http_request=request,
363
+ http_response_object_callback=HttpObjectCallback(client_id=client_id),
364
+ times=times,
365
+ time_to_live=time_to_live,
366
+ )
367
+ return await self.upsert(expectation)
368
+
369
+ async def mock_with_forward_callback(
370
+ self,
371
+ request: HttpRequest,
372
+ forward_callback: Callable,
373
+ response_callback: Callable | None = None,
374
+ times: Times | None = None,
375
+ time_to_live: TimeToLive | None = None,
376
+ ) -> list[Expectation]:
377
+ client_id = await self._register_websocket_callback(
378
+ "forward", forward_callback, response_callback
379
+ )
380
+ obj_callback = HttpObjectCallback(client_id=client_id)
381
+ if response_callback is not None:
382
+ obj_callback.response_callback = True
383
+ expectation = Expectation(
384
+ http_request=request,
385
+ http_forward_object_callback=obj_callback,
386
+ times=times,
387
+ time_to_live=time_to_live,
388
+ )
389
+ return await self.upsert(expectation)
390
+
391
+ async def _register_websocket_callback(
392
+ self,
393
+ callback_type: str,
394
+ callback_fn: Callable,
395
+ forward_response_fn: Callable | None = None,
396
+ ) -> str:
397
+ ws_client = MockServerWebSocketClient()
398
+ client_id = await ws_client.connect(
399
+ self._host,
400
+ self._port,
401
+ self._context_path,
402
+ self._secure,
403
+ self._ca_cert_path,
404
+ tls_verify=self._tls_verify,
405
+ )
406
+
407
+ if callback_type == "response":
408
+ ws_client.register_response_callback(callback_fn)
409
+ elif callback_type == "forward":
410
+ ws_client.register_forward_callback(callback_fn, forward_response_fn)
411
+
412
+ ws_client._listen_task = asyncio.create_task(ws_client.listen())
413
+ self._websocket_clients.append(ws_client)
414
+ return client_id
415
+
416
+ async def close(self) -> None:
417
+ for ws_client in self._websocket_clients:
418
+ await ws_client.close()
419
+ self._websocket_clients.clear()
420
+
421
+ async def __aenter__(self) -> AsyncMockServerClient:
422
+ return self
423
+
424
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
425
+ await self.close()